はじめに
Springフレームワークでbean定義する方法はいくつかありますが、XML定義ファイルを利用する場合は、以下のようにbeanタグで各beanを記述するのが基本です。
<bean id="file" class="java.io.File">
<constructor-arg value="/tmp" />
<property name="writable" value="true" />
</bean>
Springが提供するタグを利用するとどのようなbeanでも基本的には記述できますが、汎用的であるがゆえに、冗長な定義になりがちです。これを解決するため、Springはユーザが自由にカスタムタグを拡張する機能を提供しています。この機能を利用すると、上記のbean定義は、例えば以下のようによりシンプルに記述できるようになります。
<custom:file id="file" path="/tmp" writable="true"/>
本記事では、まず基本的なカスタムタグの作成手順について解説した後、カスタムタグを利用した簡易的なXML言語を作成してみます。以下のサンプルはこのXML言語による階乗計算プログラムの例です。
<xmlang:program argument="in">
<xmlang:var name="n" expression="1"/>
<xmlang:var name="result" expression="1" />
<xmlang:while expression="#n le #in" >
<xmlang:var name="result" expression="#n * #result" />
<xmlang:var name="n" expression="#n + 1" />
</xmlang:while>
<xmlang:return expression="#result" />
</xmlang:program>
基本的なカスタムタグの作成方法
カスタムタグの作成方法をステップバイステップで説明します。より詳しい内容は、
Springの公式ドキュメントを参照してください。
作成するカスタムタグ
java.io.File オブジェクトを生成する以下のようなカスタムタグを作成してみます。
<custom:file id="file" path="/tmp" writable="true"/>
上記のカスタムタグにより、次の java.util.File オブジェクトが生成されるものとします。
File file = new File("/tmp");
file.setWritable(true);
準備
本記事では、mavenを利用してプロジェクトを作成します。まずは、mavenで以下のようにプロジェクトを作成してください。
$ mvn archetype:generate -DgroupId=sample -DartifactId=spring-extension-sample -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
次に、pom.xmlファイルの依存に、springフレームワークを追加します。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
</dependencies>
カスタムタグの作成手順
カスタムタグの作成手順は以下のようになります。
- カスタムタグを定義するXMLスキーマを作成する
- BeanDefinitionParser(Javaクラス)を作成する。BeanDefinitionParserは、カスタムタグからBean(Javaオブジェクト)を生成する責務を持ちます。
- NamespaceHandler(Javaクラス)を作成する。NamespaceHanderは、カスタムタグとBeanDefinitionParserを関連付ける責務を持ちます。
- カスタムタグのネームスペースとNamespaceHandlerを関連付ける、 META-INF/spring.handers ファイルを作成する。
- カスタムタグのスキーマロケーションと実際のスキーマロケーションを関連付ける、META-INF/spring.schemas ファイルを作成する。
プロジェクト構成
上記の手順で作成するプロジェクト構成は以下のようになります。
.
|-- pom.xml
|-- src
| |-- main
| | |-- java
| | | `-- sample
| | | |-- FileBeanDefinitionParser.java //2.BeanDefinitionParserクラス
| | | `-- NamespaceHandler.java //3.NamespaceHanderクラス
| | `-- resources
| | |-- META-INF
| | | |-- spring.handlers //4.spring.handersファイル
| | | `-- spring.schemas //5.spring.schemasファイル
| | `-- file.xsd //1.XMLスキーマファイル
| `-- test
| |-- java
| | `-- Sample.java //確認用Javaクラス
| `-- resources
| `-- bean.xml //カスタムタグを使った確認用のbean定義ファイル
`-- target
XMLスキーマファイルの作成
スキーマファイルは以下のようになります。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.sample.com/custom" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.sample.com/custom" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<!-- springのbeasnスキーマファイルの読み込み -->
<xsd:import namespace="http://www.springframework.org/schema/beans" />
<xsd:element name="file">
<xsd:complexType>
<xsd:complexContent>
<!-- springが提供する型(id属性のみを持つ型)を拡張-->
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="path" type="xsd:string" />
<xsd:attribute name="writable" type="xsd:boolean" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
Springがid属性のみ持つBeanのベースとなる型を定義しているため、その型を拡張して作成しています。
BeanDefinitionParserの作成
カスタムタグからBeanを生成するJavaクラスを作成します。
package sample;
import java.io.File;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.w3c.dom.Element;
//fileカスタムタグのBeanDefinitionParser。
//通常はAbstractSingleBeanDefinitionParserを拡張して作成すればよい。
public class FileBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
//対象となるBeanの型を返す。
protected Class<?> getBeanClass(Element element) {
return File.class;
}
//Bean定義ファイル内の、fileカスタムタグ要素が、引数のelementとなる。
//引数のbeanはBeanを作成する責務を持つビルダークラス
protected void doParse(Element element, BeanDefinitionBuilder bean) {
String path = element.getAttribute("path");
String writable = element.getAttribute("writable");
bean
//ビルダーに対して、Bean(この例ではFileオブジェクト)のコンストラクタ引数を設定する
.addConstructorArgValue(path)
//ビルダーに対して、Bean(この例ではFileオブジェクト)のプロパティを設定する
.addPropertyValue("writable", Boolean.valueOf(writable));
}
}
子要素を持たないような単純なカスタムタグの場合、上記のようにAbstractSingleBeanDefinitionParserを継承して作成するのが楽です。doParseメソッドには、Bean定義ファイル内の対象のカスタムタグ要素が渡されるため、この要素から必要な属性値などを取り出して、Beanを生成します。Beanの生成はBeanDefinitionBuilder(ビルダークラス)を通して行います。
NamespaceHandlerの作成
NamespaceHanderは、カスタムタグの要素名とそのBeanDefinitionParserを紐付けます。
package sample;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class NamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
//1つ目の引数はカスタムタグの要素名
//2つ目の引数は対象のBeanDefinitionPaerser
registerBeanDefinitionParser("file", new FileBeanDefinitionParser());
}
}
META-INF/spring.handers ファイルの作成
spring.handlersにより、作成したXMLスキーマのネームスペースとNamespaceHanderを関連付けます。作成したファイルはMETA-INFフォルダ内に配置しなければなりません。
http\://www.sample.com/custom=sample.NamespaceHandler
コロン(:)は、Javaのプロパティファイルのメタ記号であるため、バックスラッシュ(\)でエスケープしています。イコール記号の左辺にはXMLスキーマのネームスペースを記述します。これは、XMLスキーマのtargetNamespace属性値と一致しなければなりません。イコール記号の右辺には、NamespaceHanderの完全修飾子を記述します。
META-INF/spring.schemas ファイルの作成
spring.schemasにより、作成したXMLスキーマロケーションと実際のスキーマファイルを関連付けます。作成したファイルはMETA-INFフォルダに配置しなければなりません。
http\://www.sample.com/custom.xsd=file.xsd
イコール記号の左辺にはスキーマロケーションのURIを指定します。右辺には実際のスキーマファイルのリソースパスを記述します。上記の例では、file.xsdはトップレベルのパッケージに含まれていますが、hogeパッケージ配下にある場合は、hoge/file.xsd
のように記述してください。
確認
上記で作成したカスタムタグが実際に使えるか確認してみます。まずは、カスタムタグを利用したBean定義ファイルを作成します。
<?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:custom="http://www.sample.com/custom"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.sample.com/custom http://www.sample.com/custom.xsd">
<custom:file id="file" path="/tmp" writable="true"/>
</beans>
上記のxsi:schemaLocationで指定するカスタムタグのスキーマロケーションには、spring.schemasで指定したURIを指定します。(この例では、http://www.sample.com/custom.xsd
)
次に、Springコンテナを立ち上げ、上記で定義したFileオブジェクトを取得する下記のサンプルコードを実行してみます。
import java.io.File;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Sample {
public static void main(String[] args) {
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("bean.xml")) {
File file = context.getBean(File.class);
System.out.println("file path : " + file.getAbsolutePath());
}
}
}
コンソールに以下が表示されれば成功です。
file path : /tmp
カスタムタグによる簡易XML言語
基本的なカスタムタグの作成方法はこれで分かりました(?)。次に、もう少し複雑な例として、カスタムタグを利用して、簡易XML言語を実現してみました。コードはGitHubに置いてあります。
対象言語
対象となる言語をBNFライクに定義します。
<program> ::= program argument <block>
<block> ::= <statement> | <statement> <block>
<statement> ::= <var> | <return> | <if> | <while>
<var> ::= var name expression
<return> ::= return expression
<if> ::= if expression <block>
<while> ::= while expression <block>
- プログラム(prograrm)は、programキーワード、引数名(argument)、および、ブロックから構成されます。
- ブロック(block)は文(statement)の並びです。
- 文(statement)は変数宣言文(var)、リターン文(return)、if文(if)、および、while文(while)の何れかです。
- 変数宣言文(var)は、varキーワード、式(expression)、および、ブロック(block)から構成されます。
- リターン文(return)は、returnキーワードと式(expression)からなります。
- if文(if)は、ifキーワードと式(expression)およびブロック(block)から構成されます。
- while文(while)は、whileキーワードと式(expression)およびブロック(block)から構成されます。
ここで、式(expression)としては、実装を簡単(楽?)にするため、Spring Expression Language (SpEL)を記述できるものとします。SpELはJSPのEL式のような式言語です。(SpELについての内容は、上の公式ドキュメントを参照してください。)
この言語によるプログラムは、引数を1つだけ受け取ることができます。引数名は、argumentです。その他の、各文の意味はJavaと同じなので説明は省略します。
XMLスキーマ
XMLスキーマで記述すると以下のようになります。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.sample.com/xmlang"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.sample.com/xmlang"
elementFormDefault="qualified" attributeFormDefault="unqualified">
<xsd:element name="program">
<xsd:complexType>
<xsd:sequence>
<xsd:group ref="block" />
</xsd:sequence>
<xsd:attribute name="argument" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="var">
<xsd:complexType>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attributeGroup ref="exp" />
</xsd:complexType>
</xsd:element>
<xsd:element name="return">
<xsd:complexType>
<xsd:attributeGroup ref="exp" />
</xsd:complexType>
</xsd:element>
<xsd:element name="if">
<xsd:complexType>
<xsd:sequence>
<xsd:group ref="block" />
</xsd:sequence>
<xsd:attributeGroup ref="exp" />
</xsd:complexType>
</xsd:element>
<xsd:element name="while">
<xsd:complexType>
<xsd:sequence>
<xsd:group ref="block" />
</xsd:sequence>
<xsd:attributeGroup ref="exp" />
</xsd:complexType>
</xsd:element>
<xsd:attributeGroup name="exp">
<xsd:attribute name="expression" type="xsd:string" use="required" />
</xsd:attributeGroup>
<xsd:group name="block">
<xsd:sequence>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="var" />
<xsd:element ref="if" />
<xsd:element ref="while" />
<xsd:element ref="return" />
</xsd:choice>
</xsd:sequence>
</xsd:group>
</xsd:schema>
プログラム例
さて、この言語による、プログラムは以下の通りになります。これは、与えられた数の階乗を計算するプログラムとなります。もちろん、プログラムはBeanのカスタムタグで構成されます。
<?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:xmlang="http://www.sample.com/xmlang"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.sample.com/xmlang http://www.sample.com/xmlang.xsd">
<xmlang:program argument="in">
<xmlang:var name="n" expression="1"/>
<xmlang:var name="result" expression="1" />
<xmlang:while expression="#n le #in" >
<xmlang:var name="result" expression="#n * #result" />
<xmlang:var name="n" expression="#n + 1" />
</xmlang:while>
<xmlang:return expression="#result" />
</xmlang:program>
</beans>
このプログラムは、下のJavaのコードとほぼ同等です。
public static int factorial(int in) {
int n = 1;
int result = 1;
while (n <= in) {
result = result * n;
n = n + 1;
}
return result;
}
実行例
上記のプログラムを実行するコードが以下になります。
package sample;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import sample.program.Program;
public class ProgramTest {
public static void main(String[] args) {
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("factorial.xml")) {
Program p = context.getBean(Program.class);
Object result1 = p.evaluate(3);
System.out.println("result : " + result1.toString());
Object result2 = p.evaluate(10);
System.out.println("result : " + result2.toString());
}
}
}
実際に実行すると、次のように表示されます。なんとかうまく動いているようです。
result : 6
result : 3628800