Spring Web Serviceでサーバを作成したときにハマったことメモ

あまり活躍する機会はないと思いますが、Spring Boot で Spring Web Serviceを使ってサーバを構築したときにハマったことと、解決方法のメモを残します。

## バージョン
* Spring Boot

1.4.1.RELEASE


* Spring Web Service

2.4.0.RELEASE


* Java

1.8

## サンプルコード
大半は、Spring Web Serviceの[Getting Started Guide](https://spring.io/guides/gs/producing-web-service/) を参考にしています。

```Application.java
package sample;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
	public Logger logger(InjectionPoint point) {
		return LoggerFactory.getLogger(point.getMember().getDeclaringClass());
	}
}
```
``` WebServiceConfig.java
package sample;

import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;

@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
    @Bean
    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "/ws/*");
    }

    @Bean(name = "Hello")
    public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema schemas) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("HelloPort");
        wsdl11Definition.setLocationUri("/ws");
        wsdl11Definition.setTargetNamespace("http://ijufumi.jp/sample");
        wsdl11Definition.setSchema(schemas);
        wsdl11Definition.setCreateSoap11Binding(true);
        wsdl11Definition.setCreateSoap12Binding(true);
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema schemas() {
        return new SimpleXsdSchema(new ClassPathResource("Hello.xsd"));
    }
}
```
``` Endpoint.java
package sample;

import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

import javax.annotation.PostConstruct;

@Endpoint
public class Endpoint {

    @Autowired
    Logger logger;

    @PostConstruct
    public void postConstruct() {
        logger.info("Endpoint#postConstruct() called.");
    }

    @PayloadRoot(namespace = "http://ijufumi.jp/hello", localPart = "HelloRequest")
    @ResponsePayload
    public HelloResponseType hello(@RequestPayload HelloRequestType request) {
        HelloResponseType response = new HelloResponseType();
        response.setMessage("Hello World, " + request.getName()); 
        return response;
    }
}
```
``` HelloRequestType.java
package sample;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "HelloRequestType", namespace = "http://ijufumi.jp/hello", propOrder = {
    "name"
})
public class HelloRequestType {
    @XmlElement(name = "Name")
    protected String name;

    public String getName() {
      return name;
    }
    public void setName(String name) {
      this.name = name;
    }
}
```
``` HelloResponseType.java
package sample;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "HelloResponseType", namespace = "http://ijufumi.jp/hello", propOrder = {
    "message"
})
public class HelloRequestType {
    @XmlElement(name = "Message")
    protected String message;

    public String getMessage() {
      return message;
    }
    public void setMessage(String message) {
      this.message = message;
    }
}
```
``` hello.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://ijufumi.jp/hello"
           targetNamespace="http://ijufumi.jp/hello" elementFormDefault="qualified">
    <xs:element name="HelloRequest" type="tns:HelloRequestType" />
    <xs:element name="HelloResponse" type="tns:HelloResponseType" />

    <xs:complexType name="HelloRequestType">
            <xs:sequence>
                <xs:element name="Name" type="xs:string"/>
            </xs:sequence>
    </xs:complexType>

    <xs:complexType name="HelloResponseType">
            <xs:sequence>
                <xs:element name="Message" type="xs:string"/>
            </xs:sequence>
    </xs:complexType>
</xs:schema>
```
``` request.xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                                  xmlns:h="http://ijufumi.jp/hello">
   <soapenv:Header/>
   <soapenv:Body>
     <h:HelloRequest>
       <Name>Iju</Name>
     </h:HelloRequest>
   </soapenv:Body>
</soapenv:Envelope>
```

## ハマったポイント
### 「No adapter for endpoint」
#### 事象
SOAPのXMLをPOSTすると、

```xml
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Header/>
  <SOAP-ENV:Body>
    <SOAP-ENV:Fault>
      <faultcode>SOAP-ENV:Server</faultcode>
      <faultstring xml:lang="en">No adapter for endpoint [public   sample.HelloResponseType sample.Endpoint.hello(sample.HelloRequestType) ]: Is your endpoint annotated with @Endpoint, or does it implement a supported interface like MessageHandler or PayloadEndpoint?
      </faultstring>
    </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
```

というのが返ってきました。

#### 原因
[StackOverFlow](http://stackoverflow.com/questions/15282656/no-adapter-for-endpoint-in-spring-web-service-response)でも回答されている通り、JAXBで作成したPOJOに`@XmlRootElement`がないのが原因みたいです。

#### 対応内容
リクエスト、レスポンスの両方のPOJOに`@XmlRootElement`を追加しました。

``` HelloRequestType.java
package sample;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "HelloRequestType", namespace = "http://ijufumi.jp/hello", propOrder = {
    "name"
})
public class HelloRequestType {
    @XmlElement(name = "Name")
    protected String name;

    public String getName() {
      return name;
    }
    public void setName(String name) {
      this.name = name;
    }
}
```
``` HelloResponseType.java
package sample;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "HelloResponseType", namespace = "http://ijufumi.jp/hello", propOrder = {
    "message"
})
public class HelloRequestType {
    @XmlElement(name = "Message")
    protected String message;

    public String getMessage() {
      return message;
    }
    public void setMessage(String message) {
      this.message = message;
    }
}
```

### 「unexpected element」
#### 事象
`@XmlRootElement`を追加後に再度POSTしたら、

```xml
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Header/>
  <SOAP-ENV:Body>
    <SOAP-ENV:Fault>
      <faultcode>SOAP-ENV:Server</faultcode>
      <faultstring xml:lang="en">unexpected element (uri:"http://ijufumi.jp/hello", local:"HelloRequest"). Expected elements are &lt;・・・</faultstring>
    </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
```

というのが返ってきました。

#### 原因
Endpointのメソッドの引数に、`JAXBElement`を使う必要があるようです。
ただ、この対応内容を見つけたソースを見失ってしまったので、見つけ次第更新します。

#### 対応内容
Endpointを修正。

``` Endpoint.java
package sample;

import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

import javax.annotation.PostConstruct;
import javax.xml.bind.JAXBElement;

@Endpoint
public class Endpoint {

    @Autowired
    Logger logger;

    @PostConstruct
    public void postConstruct() {
        logger.info("Endpoint#postConstruct() called.");
    }

    @PayloadRoot(namespace = "http://ijufumi.jp/hello", localPart = "HelloRequest")
    @ResponsePayload
    public HelloResponseType hello(@RequestPayload JAXBElement<HelloRequestType> request) {
        HelloResponseType response = new HelloResponseType();
        response.setMessage("Hello World, " + request.getName()); 
        return response;
    }
}
```

### 最後に
ハマったときに日本語の情報がなかったのでとりあえずメモを残しました。
ただ、行き当たりばったりで調べたものなので多少なり間違っている記述があると思うので、
適宜指摘いただけると助かります。