Appendix B. Extensible XML authoring

B.1. Introduction

Since version 2.0, Spring has featured a mechanism for schema-based extensions to the basic Spring XML format for defining and configuring beans. This section is devoted to detailing how you would go about writing your own custom XML bean definition parsers and integrating such parsers into the Spring IoC container.

To facilitate the authoring of configuration files using a schema-aware XML editor, Spring's extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring's current XML configuration extensions that come with the standard Spring distribution, please first read the appendix entitled Appendix A, XML Schema-based configuration.

Creating new XML configuration extensions can be done by following a (relatively) simple process of authoring an XML schema, coding a NamespaceHandler implementation, coding one or more BeanDefinitionParser instances and registering the NamespaceHandler and the schema in a dedicated properties file. What follows is a description of each of these steps. In the example, we'll create an XML extension (a custom XML element) that allows us to configure objects of type SimpleDateFormat directly in a Spring IoC container.

B.2. Authoring the schema

Creating an XML configuration extension for use with Spring's IoC container starts with authoring an XML Schema to describe the extension. What follows is the schema we'll use to configure SimpleDateFormat objects. The emphasized line contains an extension base for all tags that will be identifiable (meaning they have an id attribute that will be used as the bean identifier in the container).

#### myns.xsd (inside package org/springframework/samples/xml)

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.springframework.org/schema/myns"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:beans="http://www.springframework.org/schema/beans"
    targetNamespace="http://www.mycompany.com/schema/myns"
    elementFormDefault="qualified"
    attributeFormDefault="unqualified">

   <xsd:import namespace="http://www.springframework.org/schema/beans"/>
   
   <xsd:element name="dateformat">
      <xsd:complexType>
         <xsd:complexContent>
            <xsd:extension base="beans:identifiedType">
               <xsd:attribute name="lenient" type="xsd:boolean"/>
               <xsd:attribute name="pattern" type="xsd:string" use="required"/>
            </xsd:extension>
         </xsd:complexContent>
      </xsd:complexType>
   </xsd:element>
   
</xsd:schema>

The above schema will be used to configure SimpleDateFormat objects, directly in an XML application context file using the myns:dateformat configuration directive. As noted above, the id attribute will (in this case) be used as the bean identifier for the SimpleDateFormat bean.

<myns:dateformat id="dateFormat" 
    pattern="yyyy-MM-dd HH:mm"
    lenient="true"/>

Note that after we've created the infrastructure classes, the above snippet of XML will essentially be exactly the same as the following XML snippet. In other words, we're just creating a bean in the container, identified by dateFormat of type SimpleDateFormat, with a couple of properties set.

<bean id="dateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg value="yyyy-HH-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>
[Note]Note

The schema-based approach to creating configuration format, allows for tight integration with an IDE that has a schema-aware XML editor. Using a properly authored schema, you can for example use autocompletion to have a user choose between several configuration options defined in the enumeration.

B.3. Coding a NamespaceHandler

In addition to the schema, we need a NamespaceHandler that will parse all elements of this specific namespace Spring encounters while parsing configuration files. The NamespaceHandler should in our case take care of the parsing of the myns:dateformat element.

The NamespaceHandler interface is pretty simple in that it only features three methods:

  • init() - allows for initialization of the NamespaceHandler and will be called by Spring before the handler is used

  • BeanDefinition parse(Element element, ParserContext parserContext) - called when Spring encounters a top-level element (not nested inside a bean definition or a different namespace). This method can register bean definitions itself and/or return a bean definition.

  • BeanDefinitionHolder decorate(Node element, BeanDefinitionHolder definition, ParserContext parserContext) - called when Spring encounters an attribute or nested element of a different namespace, inside for example the Spring namespace. The decoration of one or more bean definitions is used for example with the out-of-the-box scopes Spring 2.0 comes with (see Section 3.4, “bean的作用域” for more information about scopes). We'll start by highlighting a simple example, without using decoration, after which we will show decoration in a somewhat more advanced example.

Although it is perfectly possible to code your own NamespaceHandler for the entire namespace (and hence provide code that parses each and every element in the namespace), it is often the case that each top-level XML element in a Spring XML configuration file results in a single bean definition (as in our case, where the myns:dateformat element results in a SimpleDateFormat bean definition). Spring features a couple of convenience classes that support this scenario. In this example, we'll use the most often used convenience class which is the NamespaceHandlerSupport class:

package org.springframework.samples.xml;

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

public class MyNamespaceHandler extends NamespaceHandlerSupport {
    
    public void init() {
        registerBeanDefinitionParser("dateformat", 
                new SimpleDateFormatBeanDefinitionParser());        
    }
}

B.4. Coding a BeanDefinitionParser

As you can see, the namespace handler shown above registers so-called BeanDefinitionParsers. A BeanDefinitionParser in this case will be consulted if the namespace handler encounters an XML element of the type that has been mapped to this specific bean definition parser (which is dateformat in this case). In other words, the BeanDefinitionParser is responsible for parsing one distinct top-level XML element defined in the schema. In the parser, we'll have access to the XML element (and its subelements) and the ParserContext. The latter can be used to obtain a reference to the BeanDefinitionRegistry, for instance, as seen in the example below.

package org.springframework.samples.xml;

import java.text.SimpleDateFormat;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

public class SimpleDateFormatBeanDefinitionParser implements BeanDefinitionParser {
   
   public BeanDefinition parse(Element element, ParserContext parserContext) {
      
      // create a RootBeanDefinition that will serve as configuration
      // holder for the 'pattern' attribute and the 'lenient' attribute
      RootBeanDefinition beanDef = new RootBeanDefinition();
      beanDef.setBeanClass(SimpleDateFormat.class);      

      // never null since the schema requires it 
      String pattern = element.getAttribute("pattern");
      beanDef.getConstructorArgumentValues().addGenericArgumentValue(pattern);

      String lenientString = element.getAttribute("lenient");
      if (StringUtils.hasText(lenientString)) {
         // won't throw exception if validation is turned on (boolean type set in schema)         
         beanDef.getPropertyValues().addPropertyValue("lenient", new Boolean(lenientString));
      }
      
      // retrieve the ID attribute that will serve as the bean identifier in the context 
      String id = element.getAttribute("id");
      
      // create a bean definition holder to be able to register the
      // bean definition with the bean definition registry
      // (obtained through the ParserContext)
      BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDef, id);
      
      // register the BeanDefinitionHolder (which contains the bean definition)
      // with the BeanDefinitionRegistry
      BeanDefinitionReaderUtils.registerBeanDefinition(holder, parserContext.getRegistry());
      
      return beanDef;
   }
}

[Note]Note
In the example here, we're defining a BeanDefinition and registering it with the BeanDefinitionRegistry. Note that you don't necessarily have to register a bean definition with the registry or return a bean definition from the parse() method. You are free to do whatever you want with the information given to you (i.e. the XML element) and the ParserContext.

The ParserContext provides access to following properties:

  • readerContext - provides access to the bean factory and also to the NamespaceHandlerResolver, which can optionally be used to resolve nested namespaces.

  • parserDelegate - controlling component that drives the parsing of (parts of) the configuration file. Typically you don't need to access this.

  • registry - the BeanDefinitionRegistry that allows you to register newly created BeanDefinition instances with.

  • nested - indicates whether or the XML element that is currently being processed is part of a outer bean definition (in other words, it's defined similar to traditional inner-beans).

B.5. Registering the handler and the schema

We're done implementing the NamespaceHandler and the BeanDefinitionParser that will take care of parsing the custom XML Schema for us. We now have the following artifacts:

  • org.springframework.samples.xml.MyNamespaceHandler - namespace handler that will register one or more BeanDefinitionParser instances

  • org.springframework.samples.xml.SimpleDateFormatBeanDefinitionParser - used my the namespace handler to parse elements of type dateformat

  • org/springframework/samples/xml/myns.xsd - the actual schema that will be used in the Spring configuration files (note that this file needs to be on the classpath, alongside your namespace handler and parser classes as we'll see later on)

The last thing we need to do is to get the namespace ready for use by registering it in two special purpose properties files. These properties files are both placed in the META-INF directory and can, for example, be distributed alongside your binary classes in a JAR file. Spring will automatically pick up the new namespaces and handlers once it finds the properties files on the classpath.

B.5.1. META-INF/spring.handlers

The properties file called spring.handlers contains a mapping of XML Schema URIs to namespace handler classes. So for our example, we need to specify the following here:

http\://www.mycompany.com/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

B.5.2. META-INF/spring.schemas

The properties file called spring.schemas contains a mapping of XML Schema locations (referred to along with the schema declaration in XML files that use the schema as part of the xsi:schemaLocation attribute) to classpath resources. This file is needed to prevent Spring from having to use a default EntityResolver that requires Internet access to retrieve the schema file. If you specify the mapping in this properties file, Spring will search for the schema on the classpath (in this case 'myns.xsd' in the 'org.springframework.samples.xml' package):

http\://www.mycompany.com/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd