Extensible XML authoringIntroductionSince 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
.Creating new XML configuration extensions can be done by following these (relatively)
simple steps:Authoring an XML schema to describe your custom element(s).Coding a custom NamespaceHandler
implementation (this is an easy step, don't worry).Coding one or more BeanDefinitionParser
implementations (this is where the real work is done).Registering the above artifacts with Spring (this too is an easy step).What follows is a description of each of these steps. For the example, we will create
an XML extension (a custom XML element) that allows us to configure objects of the type
SimpleDateFormat (from the java.text package)
in an easy manner. When we are done, we will be able to define bean definitions of type
SimpleDateFormat like this:
]]>(Don't worry about the fact that this example is very simple; much more
detailed examples follow afterwards. The intent in this first simple example is to walk
you through the basic steps involved.)Authoring the schemaCreating 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.<!-- myns.xsd (inside package org/springframework/samples/xml) -->]]>
]]>]]>(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). We are able to use this
attribute because we imported the Spring-provided 'beans'
namespace.)The above schema will be used to configure SimpleDateFormat
objects, directly in an XML application context file using the
<myns:dateformat/> element.
]]>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 the name
'dateFormat' of type SimpleDateFormat, with a
couple of properties set.
]]>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 use autocompletion to have a user choose between several
configuration options defined in the enumeration.Coding a NamespaceHandlerIn 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 features just three methods:init() - allows for initialization of
the NamespaceHandler and will be called by Spring
before the handler is usedBeanDefinition parse(Element, 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, BeanDefinitionHolder, ParserContext) -
called when Spring encounters an attribute or nested element of a different namespace.
The decoration of one or more bean definitions is used for example with the
out-of-the-box scopes Spring 2.0 supports.
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 a single <myns:dateformat/> element
results in a single SimpleDateFormat bean definition).
Spring features a number of convenience classes that support this scenario.
In this example, we'll make use the NamespaceHandlerSupport class:}
}The observant reader will notice that there isn't actually a whole lot of
parsing logic in this class. Indeed... the NamespaceHandlerSupport
class has a built in notion of delegation. It supports the registration of any number
of BeanDefinitionParser instances, to which it will delegate
to when it needs to parse an element in its namespace. This clean separation of concerns
allows a NamespaceHandler to handle the orchestration
of the parsing of all of the custom elements in its namespace,
while delegating to BeanDefinitionParsers to do the grunt work of the
XML parsing; this means that each BeanDefinitionParser will
contain just the logic for parsing a single custom element, as we can see in the next stepCoding a BeanDefinitionParserA BeanDefinitionParser will be used if the
NamespaceHandler encounters an XML element of the type
that has been mapped to the 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 thus its subelements too)
so that we can parse our custom XML content, as can be seen in the following example:// this will never be null since the schema explicitly requires that a value be supplied// this however is an optional propertyWe use the Spring-provided AbstractSingleBeanDefinitionParser
to handle a lot of the basic grunt work of creating a singleBeanDefinition.We supply the AbstractSingleBeanDefinitionParser superclass
with the type that our single BeanDefinition will represent.In this simple case, this is all that we need to do. The creation of our single
BeanDefinition is handled by the AbstractSingleBeanDefinitionParser
superclass, as is the extraction and setting of the bean definition's unique identifier.Registering the handler and the schemaThe coding is finished! All that remains to be done is to somehow make the Spring XML
parsing infrastructure aware of our custom element; we do this by registering our custom
namespaceHandler and custom XSD file in two special purpose
properties files. These properties files are both placed in a
'META-INF' directory in your application, and can, for
example, be distributed alongside your binary classes in a JAR file. The Spring XML parsing
infrastructurewill automatically pick up your new extension by consuming these special
properties files, the formats of which are detailed below.'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 write the
following:(The ':' character is a valid delimiter in the Java properties format,
and so the ':' character in the URI needs to be escaped with a backslash.)The first part (the key) of the key-value pair is the URI associated with your custom namespace
extension, and needs to match exactly the value of the
'targetNamespace' attribute as specified in your custom XSD schema.'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
absolutely 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):The upshot of this is that you are encouraged to deploy your XSD file(s) right alongside
the NamespaceHandler and BeanDefinitionParser
classes on the classpath.Using a custom extension in your Spring XML configurationUsing a custom extension that you yourself have implemented is no different from
using one of the 'custom' extensions that Spring provides straight out of the box. Find below
an example of using the custom <dateformat/> element developed in the
previous steps in a Spring XML configuration file.
]]><!-- as a top-level bean -->
]]><!-- as an inner bean -->]]>Meatier examplesFind below some much meatier examples of custom XML extensions.Nesting custom tags within custom tagsThis example illustrates how you might go about writing the various artifacts
required to satisfy a target of the following configuration:
]]>
]]>]]>The above configuration actually nests custom extensions within each other. The class
that is actually configured by the above <foo:component/>
element is the Component class (shown directly below). Notice
how the Component class does not expose
a setter method for the 'components' property; this makes it hard
(or rather impossible) to configure a bean definition for the Component
class using setter injection. components = new ArrayList ();
]]>// mmm, there is no setter method for the 'components' getComponents() {
return components;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}]]>The typical solution to this issue is to create a custom FactoryBean
that exposes a setter property for the 'components' property. {
private Component parent;
private List children;
public void setParent(Component parent) {
this.parent = parent;
}
public void setChildren(List children) {
this.children = children;
}
public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}
public Class getObjectType() {
return Component.class;
}
public boolean isSingleton() {
return true;
}
}]]>This is all very well, and does work nicely, but exposes a lot of Spring plumbing to the
end user. What we are going to do is write a custom extension that hides away all of this
Spring plumbing. If we stick to the steps described
previously, we'll start off by creating the XSD schema to define the structure of
our custom tag.
]]>We'll then create a custom NamespaceHandler.Next up is the custom BeanDefinitionParser. Remember
that what we are creating is a BeanDefinition describing
a ComponentFactoryBean. childElements = DomUtils.getChildElementsByTagName(element, "component");
if (childElements != null && childElements.size() > 0) {
parseChildComponents(childElements, factory);
}
return factory.getBeanDefinition();
}
private static BeanDefinition parseComponent(Element element) {
BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
component.addPropertyValue("name", element.getAttribute("name"));
return component.getBeanDefinition();
}
private static void parseChildComponents(List childElements, BeanDefinitionBuilder factory) {
ManagedList children = new ManagedList(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}]]>Lastly, the various artifacts need to be registered with the Spring XML infrastructure.# in 'META-INF/spring.handlers'# in 'META-INF/spring.schemas'Custom attributes on 'normal' elementsWriting your own custom parser and the associated artifacts isn't hard, but sometimes it
is not the right thing to do. Consider the scenario where you need to add metadata to already
existing bean definitions. In this case you certainly don't want to have to go off and write
your own entire custom extension; rather you just want to add an additional attribute
to the existing bean definition element.By way of another example, let's say that the service class that you are defining a bean
definition for a service object that will (unknown to it) be accessing a clustered
JCache, and you want to ensure that
the named JCache instance is eagerly started within the surrounding cluster:jcache:cache-name="checking.account"><!-- other dependencies here... -->]]>What we are going to do here is create another BeanDefinition
when the 'jcache:cache-name' attribute is parsed; this
BeanDefinition will then initialize the named JCache
for us. We will also modify the existing BeanDefinition for the
'checkingAccountService' so that it will have a dependency on this
new JCache-initializing BeanDefinition.// lots of JCache API calls to initialize the named cache...Now onto the custom extension. Firstly, the authoring of the XSD schema describing the
custom attribute (quite easy in this case).
]]>Next, the associated NamespaceHandler.Next, the parser. Note that in this case, because we are going to be parsing an XML
attribute, we write a BeanDefinitionDecorator rather than a
BeanDefinitionParser.Lastly, the various artifacts need to be registered with the Spring XML infrastructure.# in 'META-INF/spring.handlers'# in 'META-INF/spring.schemas'Further ResourcesFind below links to further resources concerning XML Schema and the extensible XML support
described in this chapter.The XML Schema Part 1: Structures Second EditionThe XML Schema Part 2: Datatypes Second Edition