matsukaz's blog

Agile, node.js, ruby, AWS, cocos2d-xなどなどいろいろやってます

Spring Web Services まとめ その2

Webサービスの実装 (Chapter 3)

contract-firstなWebサービス開発を行う際は、実際に送受信されるXMLメッセージ構造を意識することが最も重要。

XMLメッセージ構造

ここでは、休暇申請情報を表す以下のXMLメッセージ*1を例とする。

<HolidayRequest xmlns="http://mycompany.com/hr/schemas">
    <Holiday>
        <StartDate>2006-07-03</StartDate>
        <EndDate>2006-07-07</EndDate>
    </Holiday>
    <Employee>
        <Number>42</Number>
        <FirstName>Arjen</FirstName>
        <LastName>Poutsma</LastName>
    </Employee>
</HolidayRequest>
XMLデータ定義

ここでは、XML Schemaを利用してXMLデータを定義する。以下は、上記の休暇申請情報を定義したXML Schemaである。

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:hr="http://mycompany.com/hr/schemas"
        elementFormDefault="qualified"
        targetNamespace="http://mycompany.com/hr/schemas">
    <xs:element name="HolidayRequest">
        <xs:complexType>
            <xs:all>
                <xs:element name="Holiday" type="hr:HolidayType"/>
                <xs:element name="Employee" type="hr:EmployeeType"/>
            </xs:all>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="HolidayType">
        <xs:sequence>
            <xs:element name="StartDate" type="xs:date"/>
            <xs:element name="EndDate" type="xs:date"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="EmployeeType">
        <xs:sequence>
            <xs:element name="Number" type="xs:integer"/>
            <xs:element name="FirstName" type="xs:string"/>
            <xs:element name="LastName" type="xs:string"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>
サービス定義(WSDL)

Spring-WSでは、WSDLの作成は必須ではない。XML Schemaの定義といくつかのコンフィギュレーションにより、Spring-WSが代わりにWSDLを公開してくれるからである。
ここでは、手作業でWSDLを作成した場合の記述例を示す。

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
                  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
                  xmlns:schema="http://mycompany.com/hr/schemas"
                  xmlns:tns="http://mycompany.com/hr/definitions"
                  targetNamespace="http://mycompany.com/hr/definitions">
    <wsdl:types>
        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
            <xsd:import namespace="http://mycompany.com/hr/schemas"
                schemaLocation="hr.xsd"/>
        </xsd:schema>
    </wsdl:types>
    <wsdl:message name="HolidayRequest">
        <wsdl:part element="schema:HolidayRequest" name="HolidayRequest"/>
    </wsdl:message>
    <wsdl:portType name="HumanResource">
        <wsdl:operation name="Holiday">
            <wsdl:input message="tns:HolidayRequest" name="HolidayRequest"/>
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="HumanResourceBinding" type="tns:HumanResource">
        <soap:binding style="document"
            transport="http://schemas.xmlsoap.org/soap/http"/>
        <wsdl:operation name="Holiday">
            <soap:operation soapAction="http://mycompany.com/RequestHoliday"/>
            <wsdl:input name="HolidayRequest">
                <soap:body use="literal"/>
            </wsdl:input>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="HumanResourceService">
        <wsdl:port binding="tns:HumanResourceBinding" name="HumanResourcePort">
            <soap:address location="http://localhost:8080/holidayService/"/>
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>
プロジェクトの作成

ここでは、Maven2*2を利用したプロジェクトを作成する。

mvn archetype:create -DarchetypeGroupId=org.springframework.ws -DarchetypeArtifactId=spring-ws-archetype -DarchetypeVersion=1.0-rc2 -DgroupId=com.mycompany.hr -DartifactId=holidayService

上記のコマンドにより、holidayServiceディレクトリが作成される。
続けて"/src/main/webapp"ディレクトリ配下の"WEB-INF/web.xml"を以下の内容に編集する。

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
             http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">

    <display-name>MyCompany HR Holiday Service</display-name>

    <!-- take especial notice of the name of this servlet -->
    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

また、"WEB-INF/spring-ws-servlet.xml"ファイル*3も合わせて作成しておく。このファイルには、Spring-WSで利用するBeanを定義していく。

エンドポイントの実装

Spring-WSでは、受け取ったXMLメッセージをハンドリングするため、以下のいずれかのエンドポイントを実装する必要がある。

    • message endpoints
      • SOAPヘッダを含む全てのXMLメッセージをハンドリングできる。
    • payload endpoints
      • SOAPボディ内のXMLメッセージをハンドリングできる。

一般的には、SOAPメッセージではなくpayloadのみハンドリングしたいため、payload endpointsを実装する。
以下は、JDomを利用した payload endpointsの実装例である*4。JDomを利用したpayload endpointsを実装する場合は、AbstractJDomPayloadEndpointを継承したクラスを実装する。

package com.mycompany.hr.ws;

import java.text.SimpleDateFormat;
import java.util.Date;

import com.mycompany.hr.service.HumanResourceService;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.xpath.XPath;
import org.springframework.ws.server.endpoint.AbstractJDomPayloadEndpoint;

public class HolidayEndpoint extends AbstractJDomPayloadEndpoint {

    private XPath startDateExpression;

    private XPath endDateExpression;

    private XPath nameExpression;

    private final HumanResourceService humanResourceService;

    public HolidayEndpoint(HumanResourceService humanResourceService) {
        this.humanResourceService = humanResourceService;
        Namespace namespace = Namespace.getNamespace("hr", "http://mycompany.com/hr/schemas");
        startDateExpression = XPath.newInstance("//hr:StartDate");
        startDateExpression.addNamespace(namespace);
        endDateExpression = XPath.newInstance("//hr:EndDate");
        endDateExpression.addNamespace(namespace);
        nameExpression = XPath.newInstance("concat(//hr:FirstName,' ',//hr:LastName)");
        nameExpression.addNamespace(namespace);
    }

    protected Element invokeInternal(Element holidayRequest) throws Exception {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date startDate = dateFormat.parse(startDateExpression.valueOf(holidayRequest));
        Date endDate = dateFormat.parse(endDateExpression.valueOf(holidayRequest));
        String name = nameExpression.valueOf(holidayRequest);

        humanResourceService.bookHoliday(startDate, endDate, name);
        return null;
    }
}

コンストラクタでHumanResourceServiceを受け取っているが、これはSpring Frameworkコンストラクタ・インジェクションを利用して実装クラスの参照を渡すことができる。
invokeInternal()はテンプレートメソッドであり、引数のelementには要素のXMLメッセージが渡される。受け取ったElementに対しては、JDomを利用して自由に操作できる。
また、invokerInternal()の戻り値はElementとなっているが、サービスのレスポンスが存在する場合は、XMLメッセージ作成して返す必要がある。ここでは、サービスのレスポンスはないため、nullを返している。
以下は、上記の実装を行うためのMaven2のpom.xmlファイルの例である。

<dependencies>
    <dependency>
        <groupId>org.springframework.ws</groupId>
        <artifactId>spring-ws-core</artifactId>
        <version>1.0-rc2</version>
    </dependency>
    <dependency>
        <groupId>jdom</groupId>
        <artifactId>jdom</artifactId>
        <version>1.0</version>
    </dependency>
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1</version>
    </dependency>
    <dependency>
        <groupId>javax.xml.soap</groupId>
        <artifactId>saaj-api</artifactId>
        <version>1.3</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.sun.xml.messaging.saaj</groupId>
        <artifactId>saaj-impl</artifactId>
        <version>1.3</version>
        <scope>runtime</scope>
    </dependency>
<dependencies>

最後に、作成したpayload endpoints実装をSpring-WSの設定ファイル(spring-ws-servlet.xml)に記述する。

<beans xmlns="http://www.springframework.org/schema/beans">

    <bean id="holidayEndpoint" class="com.mycompany.hr.ws.HolidayEndpoint">
        <constructor-arg ref="hrService"/>
    </bean>

    <bean id="hrService" class="com.mycompany.hr.service.StubHumanResourceService"/>

</beans>

は、作成した実装クラスをSpring-WSに登録するための記述である。
また、メッセージのルーティング設定を行うため、以下の記述も追記する。

    <bean class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
        <property name="mappings">
            <props>
                <prop key="{http://mycompany.com/hr/schemas}HolidayRequest">holidayEndpoint</prop>
            </props>
        </property>
        <property name="interceptors">
            <bean class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor"/>
        </property>
    </bean>

この設定により、"http://mycompany.com/hr/schemas"名前空間メッセージを受け取った場合は、holidayEndpointへとメッセージがルーティングされる。
WSDLを公開するために、さらに以下の記述も追記する*5

    <bean id="holiday" class="org.springframework.ws.wsdl.wsdl11.DynamicWsdl11Definition">
        <property name="builder">
            <bean class="org.springframework.ws.wsdl.wsdl11.builder.XsdBasedSoap11Wsdl4jDefinitionBuilder">
                <property name="schema" value="/WEB-INF/hr.xsd"/>
                <property name="portTypeName" value="HumanResource"/>
                <property name="locationUri" value="http://localhost:8080/holidayService/"/>
                <property name="targetNamespace" value="http://mycompany.com/hr/definitions"/>
            </bean>
        </property>
    </bean>
</beans>

以上より、http://localhost:8080/holidayService/holiday.wsdlにアクセスすることでWSDLが取得可能となる。また、http://localhost:8080/holidayService/でサービスを実行できる*6
作成したプロジェクトは、mvn installを実行してWARファイルを作成し、TomcatなどのAPサーバにデプロイすることで実行可能となる。

*1:のように、1つのルートエレメントで囲んだメッセージ形式をwrappedパターンと呼ぶ。

*2:Maven2についてはMaven2のTipsを集めるWikiなどを参考に。

*3:で指定した名前の後ろに-servlet.xmlを付けたファイル名となる。

*4:JDomは実装例の1つであり、DOM、dom4j、XOM、SAX、StAX、JAXB、Castor、XMLBeans、JiBX、XStreamなど、様々な技術で実装することができる

*5:WSDLを手動で作成した場合は、この定義は必要ない

*6:サービスの実行にはsoapUIなどが便利