自定义标签解析 _ JAVA

之前我们提到过Spring中存在默认标签与自定义标签两种,上一篇中我们分析了Spring中对默认标签的解析过程,下面我们开始分析Spring中自定义标签的加载过程。前面代码当完成从配置文件到Document的转换并提取对应的root后,将开始了所有元素的解析,而在这一过程中便开始了默认标签与自定义标签两种格式的区分,函数如下:

  protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    if (delegate.isDefaultNamespace(root)) {
     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 {
          delegate.parseCustomElement(ele);
         }
       }
     }
    }
    else {
     delegate.parseCustomElement(root);
    }
  }

从上面的代码中可以看出,当Spring拿到一个元素时首先要做的是根据命名空间进行解析,如果是默认命名空间,则使用parseDefaultElement方法进行元素解析,否则使用parseCustomElement方法进行解析。在分析自定义标签解析过程前,先了解一下自定义标签的使用。

自定义标签解析 _ JAVA

1、自定义标签的使用

很多情况下,我们需要为系统提供可以配置化支持,简单的做法可以直接基于Spring的标准bean来配置,但是配置较为复杂或者需要更多丰富控制的时候,会显得非常笨拙。一般做法会使用原生态的方式去解析定义好的XML文件,然后转化为配置对象。这种方式当然可以解决所有问题,但实现起来比较繁琐,特别是在配置非常复杂的时候,解析工作是一个不得不考虑的负担,Spring提供了可扩展Schema的支持,扩展Spring自定义标签配置大致需要以下几个步骤

  • 1、创建一个需要扩展的组件

  • 2、定义一个XSD文件描述组件内容

  • 3、创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中定义和组件定义

  • 4、穿件一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器

  • 5、编写Spring.handlers和Spring.schemas文件

现在我们就按照上面步骤一步步体验自定义标签的过程

1、首先创建一个POJO,这个POJO没有任何特别之处,只是用来接收配置文件

package craig.test;
public class User {
  private String id;
  private String userName;
  private String email;
  //省略get/set方法
}

2、定义一个XSD文件描述组件内容


上面的XSD文件中描述了一个新的targetNamespace,并在这个空间中定义了一个name为user的element,user有3个属性id、userName和email

3、创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和组件定义。

package test.customtag;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

	protected Class getBeanClass(Element element) {
		return User.class;
	}

	protected void doParse(Element element, BeanDefinitionBuilder bean) {
		String userName = element.getAttribute("userName");
		String email = element.getAttribute("email");
		if (StringUtils.hasText(userName)) {
			bean.addPropertyValue("userName", userName);
		}
		if (StringUtils.hasText(email)) {
			bean.addPropertyValue("email", email);
		}
	}

}

4、创建一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器中

package test.customtag;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class MyNamespaceHandler extends NamespaceHandlerSupport {

	@Override
	public void init() {
		registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
	}

}

当遇到自定义标签user:aaa这样类似于user开头的元素,就会把这个元素扔给对应UserBeanDefinitionParser去解析

5、编写Spring.handlers和Spring.schemas文件,默认位置是在工程的/META-INF/文件夹下,当然也可以通过Spring的扩展或者修改源码的方式改变路径

spring.handlers

http://www.example.org/schema/user=craig.test.MyNamespaceHandler

spring.schemas

http://www.example.org/schema/user.xsd=META-INF/user.xsd

到这里自定义的配置就结束了,而Spring加载自定义的大致流程是遇到自定义标签然后就去Spring.handlers和Spring.schemas中去找对应的handler和XSD,默认位置是/META-INF/下,进而找到对应handler以及解析元素的parser,从而完成了整个自定义元素的解析,也就是说自定义与Spring中默认的标准配置不同在于Spring将自定义标签解析的工作委托给了用户去实现。

6、创建测试配置文件,在配置文件中引入对应的命名空间,以及XSD后,便可以直接使用自定义标签了,

 

7、编写测试类

package test.customtag;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

	public static void main(String[] args) {
		ApplicationContext bf = new ClassPathXmlApplicationContext("test/customtag/custom.xml");
		User user = (User) bf.getBean("testbean");
		System.out.println(user.getUserName() + " " + user.getEmail());
	}

}

整个项目的目录结构如下图:不出意外的话,应该能看到输出结果到控制台了aaa bbb上面例子中实现了通过自定义标签通过属性的方式将user类型的Bean赋值,在Spring中自定义标签还有我们熟知的tx(tx:annotation-driven)。有兴趣的小伙伴可以了解一下。

2、自定义标签解析

了解了自定义标签的使用后,我们一起来探究一下自定义标签的解析过程

  public BeanDefinition parseCustomElement(Element ele) {
    return parseCustomElement(ele, null);
  }
  //containingBd为父类bean,对顶层元素的解析应该设置为null
  public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
    //获取对应的命名空间
    String namespaceUri = getNamespaceURI(ele);
    //根据命名空间找到对应的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));
  }

了解自定义标签的使用方法后,我们发现其实思路非常简单,根据对应的bean获取对应的命名空间,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析,下面我们看看如何获取命名空间吧

2.1、获取标签的命名空间

解析标签是从命名空间开始的,Spring中默认标签和自定义标签以及自定义标签中不同标签的处理都是以标签所提供的命名空间为基础的,至于如何提起对应的元素的命名空间其实并不需要我们亲自去实现,在org.w3c.dom.Node中已经提供了方法供我们直接调用:

  public String getNamespaceURI(Node node) {
    return node.getNamespaceURI();
  }
2.2、提取自定义标签处理器

提取完命名空间后,就可以进行NamespaceHandler的提取了,继续之前的parseCustomElement函数的跟踪,分析NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)

在readerContext初始化的时候其属性namespaceHandlerResolver已经被初始化为DefaultNamespaceHandlerResolver的实例,所以,这里调用的resolve方法其实调用的是DefaultNamespaceHandlerResolver类中的方法,我们进入DefaultNamespaceHandlerResolver的resolve方法进行查看。

DefaultNamespaceHandlerResolver.java

  public NamespaceHandler resolve(String namespaceUri) {
    //获取所有已经配置的handler映射
    MaphandlerMappings = getHandlerMappings();
    //根据命名空间找到对应的信息
    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);
       //调用自定义的NamespaceHandler的初始化方法
       namespaceHandler.init();
       //记录在缓存
       handlerMappings.put(namespaceUri, namespaceHandler);
       return namespaceHandler;
     }
     catch (ClassNotFoundException ex) {
       throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
          namespaceUri + "] not found", ex);
     }
     catch (LinkageError err) {
       throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
          namespaceUri + "]: problem with handler class file or dependent class", err);
     }
    }
  }

上面的函数阐述了解析自定义NamespaceHandler的过程,通过之前的示例程序我们了解到如果要使用自定义标签,其中必不可少的操作就是在spring.handler文件中配置命名空间与命名空间处理器的映射关系,Spring根据映射关系找到匹配的处理器,而寻找疲累的处理器就是在上面函数中实现的,当获取到自定义的NamespaceHandler之后就可以进行处理器初始化并解析了,回忆一下示例中对于命名空间处理器的内容:

package craig.test;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class MyNamespaceHandler extends NamespaceHandlerSupport {
  public void init() {
    registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
  }
}

当得到自定义命名空间处理后会马上执行namespaceHandler.init()来进行自定义BeanDefinitionParser的注册,这里可以注册多个标签解析器,当前示例中只支持tag:user的写法,也可以注册多个解析器,如tag:A、tag:B等,使得tag标签可以支持多种标签解析。

注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器进行解析,根据上面的函数与之前介绍过的例子,我们基本上可以推断getHandlerMappings的主要功能就是读取spring.handlers配置文件并将配置文件缓存在map中。

  private MapgetHandlerMappings() {
    MaphandlerMappings = this.handlerMappings;
    //如果没有被缓存则开始进行缓存
    if (handlerMappings == null) {
     synchronized (this) {
       handlerMappings = this.handlerMappings;
       if (handlerMappings == null) {
         if (logger.isDebugEnabled()) {
          logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
         }
         try {
          //this.handlerMappingsLocation在构造函数中已经被初始化为:META-INF/spring.handlers
          Properties mappings =
              PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
          if (logger.isDebugEnabled()) {
            logger.debug("Loaded NamespaceHandler mappings: " + mappings);
          }
          handlerMappings = new ConcurrentHashMap(mappings.size());
          //将Properties格式文件合并到Map格式的handlerMappings中
          CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
          this.handlerMappings = handlerMappings;
         }
         catch (IOException ex) {
          throw new IllegalStateException(
              "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
         }
       }
     }
    }
    return handlerMappings;
  }

借助了工具类PropertiesLoaderUtils对属性handlerMappingsLocation进行了配置文件的读取,handlerMappingsLocation被默认初始化为”META-INF/spring.handlers”。

2.3、标签解析

得到解析器及要分析的元素后,Spring就可以将解析工作委托给自定义解析器去解析了,在Spring中的代码为:

    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

已之前提到的示例进行分析,此时的handler已经被实例化成为了我们自定义的MyNamespaceHandler了,而MyNamespaceHandler也已经完成了初始化工作,但是在我们实现的自定义命名空间处理器中并没有实现parse方法,所以推断,这个方法是父类中的实现,查看父类NamespaceHandlerSupport中的parse方法。

NamespaceHandlerSupport.java

  public BeanDefinition parse(Element element, ParserContext parserContext) {
    return findParserForElement(element, parserContext).parse(element, parserContext);
  }

解析过程中首先是寻找元素对应的解析器,进而调用解析器中的parse方法。获取在MyNamespaceHandler类中的init方法中注册的对应的UserBeanDefinitionParser实例,并调用其parse方法进行进一步解析。

  private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    //获取元素名称,也就是中的user,若在示例中,此时localName为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;
  }

对于parse方法的处理:

  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));
         }
       }
       //将AbstractBeanDefinition转换为BeanDefinitionHolder并注册
       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) {
       parserContext.getReaderContext().error(ex.getMessage(), element);
       return null;
     }
    }
    return definition;
  }

虽说是对配置文件的解析,但是这个函数中大部分的代码是用来处理将解析后的AbstractBeanDefinition转化为BeanDefinitionHolder并注册的功能,真正去做解析的事情委托给了函数parseInternal,正是这句代码调用了我们自定义的解析函数。

在parseInternal中并不是直接调用自定义的doParse函数,而是进行了一系列的数据准备,包括对beanClass、scope、lazyInit等属性的准备。

  protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
    String parentName = getParentName(element);
    if (parentName != null) {
     builder.getRawBeanDefinition().setParentName(parentName);
    }
    //获取自定义标签中的class,此时会调用自定义解析器如UserBeanDefinitionParser中的getBeanClass方法
    Class beanClass = getBeanClass(element);
    if (beanClass != null) {
     builder.getRawBeanDefinition().setBeanClass(beanClass);
    }
    else {
     //若子类没有重写getBeanClass方法则尝试子类是否重写getBeanClassName方法
     String beanClassName = getBeanClassName(element);
     if (beanClassName != null) {
       builder.getRawBeanDefinition().setBeanClassName(beanClassName);
     }
    }
    builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
    if (parserContext.isNested()) {
     // 若存在父类则使用父类的scope属性
     builder.setScope(parserContext.getContainingBeanDefinition().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();
  }
 
  protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
    doParse(element, builder);
  }

自定义标签的处理过程,虽然在实例中我们定义UserBeanDefinitionParser,在其中我们只是做了与自己业务逻辑相关的部分,处理过程中同样也是按照Spring中默认标签的处理方式进行,包括创建BeanDefinition以及进行相应默认属性的设置,对于这些工作Spring都默默地帮我们实现了,只是暴露出一些接口来供用户实现个性化的业务。

到此我们已经完成了Spring中全部的解析工作,也就是说到现在为止我们已经理解了Spring将bean从配置文件加载到内存中的全部过程,接下的任务便是如何使用这些bean。

发布于 2020-04-19 22:35:45
收藏
分享
海报
0 条评论
176
上一篇:bean的加载 一 _ JAVA 下一篇:默认标签解析 二 _ JAVA
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码