Ever wondered how an XML based Spring configuration gets converted to Spring beans? The underlying principle is quite easy to grasp. Simply, parse the given XML document and keep creating the appropriate beans along the way.
Sample XML config
<?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:jms="http://www.springframework.org/schema/jms" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <jms:listener-container connection-factory="random"> <jms:listener ref="randomKlass" method="randomMethod" /> </jms:listener-container> <bean id="randomKlass" class="com.rudranirvan.RandomKlass"> <property name="randomName" value="randomValue" /> </bean> </beans>
Prerequisites
XSD
XSD (Xml Schema Definition) defines how an XML document is structured. One needs to adhere to this structure when creating an XML for the same. Why? Because the same XSD will be referred to by Spring while parsing the XML.
XSDs are pretty much self-explanatory. You can read the spring-beans xsd declared in the above XML here.
XML Namespace
Namespaces are no-frills conflict-avoidance mechanisms. How will you differentiate between a <body> tag of one XSD from the <body> tag of another XSD?
Simple, just namespace the tags! <ns1:body> vs <ns2:body>. ns1 and ns2 just denote two namespace prefixes.
In the above XML, the annotation-config element uses the context namespace prefix. listener-container uses the jms namespace prefix.
XML Namespace Prefix
xmlns attribute is used to define XML namespaces. Namespace prefixes provide an easy to use replacement for namespaces. For e.g., in the above XML, xmlns:jms=”http://www.springframework.org/schema/jms “ denotes that the jms prefix will point to the http://www.springframework.org/schema/jms namespace.
The <bean> element belongs to the default namespace of the document, http://www.springframework.org/schema/beans. This default namespace is defined by xmlns=”http://www.springframework.org/schema/beans “.
Part 1: Resolving the XSD files
Resolution of XSDs is a very simple step. The location where the XSDs are present is already provided in the XML document by the xsi:schemaLocation attribute. schemaLocation can have multiple entries. Each will be in the following format: “namespace namespace-schema-URL“. Example from the above XML:
http://www.springframework.org/schema/context http://www.springframework.org/schema/beans/spring-context.xsd
This tells you that the XSD for the http://www.springframework.org/schema/context schema is located at the following URI: http://www.springframework.org/schema/beans/spring-context.xsd.
Although this points to a remote HTTP resource, this spring-context XSD is not fetched from the internet! PluggableSchemaResolver helps Spring load these XSDs without accessing to the internet.
How does PluggableSchemaResolver work?
If you look at its source code, you will realize that it picks up all the META-INF/spring.schemas files from the classpath. These files are already shipped along with the appropriate spring JARs and contain the schemas’ remote URI to classpath URI mapping.
Example: The spring-context jar contains the following line in its META-INF/spring.schemas file:
http\://www.springframework.org/schema/context/spring-context.xsd=org/springframework/context/config/spring-context-4.1.xsd
It denotes that the local classpath location of the XSD is classpath:org/springframework/context/config/spring-context-4.1.xsd. This mapping from remote resource to the local classpath resource will be stored by the PluggableSchemaResolver.
Part 2: Parsing the XML document
Spring uses DefaultBeanDefinitionDocumentReader to read the above XML document and, consequently, creates instances of BeanDefinition, aka, beans. (A BeanDefinition is just a programmatic description of a Spring bean.)
If you look at the source code of DefaultBeanDefinitionDocumentReader, you will notice that it already has the schema of spring-beans.xsd (the default context) hardcoded into it. However, when it encounters a custom namespaced element, for e.g., context, it will use the appropriate NamespaceHandler for parsing the same. The NamespaceHandler to be used will be decided by the DefaultNamespaceHandlerResolver. Brief summary of the steps taken by Spring to parse an XML document:
- If the XML element belongs to the default namespace (refer XML Namespace Prefix above), DefaultBeanDefinitionDocumentReader parses, and creates the BeanDefinition for the same.
- Otherwise, for custom namespace elements, the appropriate NamespaceHandler will be used.
- Which NamespaceHandler to use is determined by DefaultNamespaceHandlerResolver.
How does DefaultNamespaceHandlerResolver work?
If you look at its source code, you will realize that it picks up all the META-INF/spring.handlers files from the classpath. These files are already shipped along with the appropriate spring JARs and contain the schema to handler mapping.
Example: The spring-context jar contains the following line in its META-INF/spring.handlers file:
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
This signifies that all the elements belonging to the http://www.springframework.org/schema/context schema will be handled by org.springframework.context.config.ContextNamespaceHandler. This mapping from namespace to NamespaceHandler will be stored by the DefaultNamespaceHandlerResolver. The same will be returned when requested by the DefaultBeanDefinitionDocumentReader.
Part 3: How are custom namespace elements converted to beans?
As we saw above, the parsing of custom namespace elements, for e.g., <context:annotation-config> or <jms:listener-container> will be handled by the appropriate NamespaceHandler. The handler in turn delegates the parsing to a BeanDefinitionParser for further processing.
Example 1: ContextNamespaceHandler will delegate the parsing job for annotation-config (context:annotation-config) to AnnotationConfigBeanDefinitionParser (click to see why).
Example 2: JmsNamespaceHandler will delegate the parsing job for listener-container (jms:listener-container) to JmsListenerContainerParser (click to see why).
Sample scenario: JmsListenerContainerParser in action
The <jms:listener-container> element will cause a JmsListenerContainerFactory to be set up. But which exact implementation to use? In our sample XML, a DefaultJmsListenerContainerFactory will be set up. Why? Because the attribute container-type (of the element listener-container) has a value of default. Refer this for the internals.
But did you notice that we have not even defined this attribute in our sample XML? Then where is it getting this value from? From the XSD! If you were to look at the spring-jms XSD, you would notice that the default value for the attribute container-type is default!
Epilogue
Remote schema XSDs are picked up from the local classpath by using the mapping defined in META-INF/spring.schemas. Custom (non-default) namespaces are handled by the appropriate NamespaceHandler. Which NamespaceHandler to use will be defined in the META-INF/spring.handlers files. NamespaceHandler in turn uses a BeanDefinitionParser.
What can you deduce from the above? You can create your own custom namespaces! Explore more here. 🙂