Good news, everyone! I’m back. With totally redesigned blog.
Today’s topic is Spring Domain Specific Language creation.
Sometimes we want to make our spring configs looks simplier. For example:
<bean id="chainedService" class="com.example.blocks.ChainXmlService" parent="baseXmlService">
<property name="styleSheet" value="xsl/sample.xsl"/>
<property name="serviceChain">
<list>
<bean class="com.example.blocks.HttpProxyBlock">
<property name="url" value="http://example.com/some/url"/>
</bean>
<ref bean="authBlock"/>
</list>
</property>
</bean>
This config might look like this:
<web:chain id="chainedService" xsl="xsl/sample.xsl">
<web:block class="com.example.blocks.HttpProxyBlock" url="http://example.com/some/url"/>
<web:block ref="authBlock"/>
</web:chain>
Looks fine. How we can do it?
First of all, lets define our namespace in spring configuration. For example: xmlns:web="http://example.com/web-ns"
Now we should create XSD-schema for our namespace and save somewhere on classpath:
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://example.com/web-ns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/web-ns"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<!-- defining element chain -->
<xsd:element name="chain">
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="block"/>
</xsd:choice>
<xsd:attribute name="id" type="xsd:ID" use="required"/>
<xsd:attribute name="xsl" type="xsd:string" use="optional"/>
</xsd:complexType>
</xsd:element>
<!-- definig element block -->
<xsd:element name="block">
<xsd:complexType>
<xsd:attribute name="class" type="xsd:string" use="optional"/>
<xsd:attribute name="ref" type="xsd:ID" use="optional"/>
<xsd:any namespace="##any" minOccurs="0"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
Then register out schema in spring. Create META-INF/spring.schemas
and put there some content:
http\://example.com/web-ns.xsd=path/to/our/schema.xsd
Now we are going to create Namespace handler and register it:
package com.example.spring;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class WebNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("chain", new ChainBeanDefinitionParser());
}
}
To register this handler – create META-INF/spring.handlers and put
there some content:
http\://example.com/web-ns=com.example.spring.WebNamespaceHandler
Now we should implement definition parser.
package com.example.spring;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
import java.util.List;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import com.example.blocks.ChainXmlService;
public class ChainBeanDefinitionParser extends AbstractBeanDefinitionParser {
@Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder root = BeanDefinitionBuilder.genericBeanDefinition();
// setting generic bean properties
root.setLazyInit(true);
root.getRawBeanDefinition().setParentName("baseXmlService");
root.getRawBeanDefinition().setBeanClass(ChainXmlService.class);
root.getRawBeanDefinition().setSource(parserContext.extractSource(element));
if (parserContext.isNested()) {
root.setScope(parserContext.getContainingBeanDefinition().getScope());
}
return parseElement(root, element, parserContext);
}
private AbstractBeanDefinition parseElement(BeanDefinitionBuilder root, Element element, ParserContext parserContext) {
if (element.hasAttribute("xsl")) {
root.addPropertyValue("styleSheet", element.getAttribute("xsl"));
}
List<Element> childs = (List<Element>) DomUtils.getChildElementsByTagName(element, "block");
if (childs != null && childs.size() > 0) {
ManagedList blocks = new ManagedList(childs.size());
for (Element child : childs) {
if (child.hasAttribute("ref")) {
blocks.add(parserContext.getRegistry().getBeanDefinition(child.getAttribute("ref")));
} else {
blocks.add(parseBlock(child));
}
}
root.addPropertyValue("serviceChain", blocks);
}
return root.getBeanDefinition();
}
private AbstractBeanDefinition parseBlock(Element element) {
BeanDefinitionBuilder block = BeanDefinitionBuilder.genericBeanDefinition(element.getAttribute("class"));
NamedNodeMap attributes = element.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Attr attr = (Attr) attributes.item(i);
String name = attr.getName();
if ("id".equals(name) || "class".equals(name)) {
continue;
}
block.addPropertyValue(name, attr.getValue());
}
return block.getBeanDefinition();
}
}
That’s all! Now you can use simple domain specific language in your
spring configuration. In my project I’ve implemented my own DSL -
config size reduced and readability increased. It’s big step to human
readable configs
P.S. Don’t forget to register schema in your IDE – you will get
absolute support for custom elements (IntelliJ IDEA totally supports
them in spring facets).
March 16, 2011 at 6:20 pm
Nice post. You can also find good documentation and examples for this feature in Spring’s reference documentation (Appendix C).
For an alternative approach (and shameless plug!), you can have a look at my project, Dynaspring. It implements an extensible DSL for Spring which is based on Lisp rather than XML. Your example would be roughly translated to
(defun block (&key ref class url)
(if ref
(ref ref)
(bean class :properties ("url" url))))
(defmacro define-chain (name (&key xsl (parent "baseXmlService")) &body blocks)
`(defbean ,name "com.example.blocks.ChainXmlService"
:parent ,parent
:properties (,@(when xsl `("styleSheet" ,xsl))
"serviceChain" (list ,@blocks))))
and used like this:
(define-chain "chainedService" (:xsl "xsl/sample.xsl")
(block :class "com.example.blocks.HttpProxyBlock" :url "http://example.com/some/url")
(block :ref "authBlock"))