【Spring Boot】SOAPクライアントの実装

はじめに
-
SpringBootでのSOAPクライアント実装方法について記載します。
あまり使われなくなったSOAPですが、たまに使うので備忘として。

環境
-
- Spring Boot 2.3
- spring-ws-core
- Java 8

ライブラリの追加
-
spring-ws-coreを依存関係に追加します。

```xml:pom.xm
<dependency>
    <groupId>org.springframework.ws</groupId>
    <artifactId>spring-ws-core</artifactId>
</dependency>
```

WSDLからソース生成
-
maven-jaxb2-pluginを使用します。
pom.xmlに下記を定義し、実行することでSOAPサービスのRequest/ResponseのBeanが生成されます。
データ定義をXSDに切り出している場合にも対応しています。

＃Gradle版がなくすみません…。いつか試して追記したいです。

```xml:pom.xml
<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <!-- WSDLからJAXBのソースを生成するためのプラグイン -->
    <plugin>
      <groupId>org.jvnet.jaxb2.maven2</groupId>
      <artifactId>maven-jaxb2-plugin</artifactId>
      <version>0.14.0</version>
      <executions>
        <!-- 以下、webサービスごとに定義 -->
        <execution>
          <id>サービスのID（任意の値）executions内で一意</id>
          <goals>
            <goal>generate</goal>
          </goals>
          <configuration>
            <!-- WSDLから生成する旨の宣言 -->
            <schemaLanguage>WSDL</schemaLanguage>
            <!-- 生成元となるWSDLとXSDの格納先 ※WSDLとXSDで同一ディレクトリ前提 -->
            <schemaDirectory>${project.basedir}/wsdl/exampleSoap</schemaDirectory>
            <!-- WSDLからXSDを参照している場合に必要となるスキーマ参照定義 -->
            <schemaIncludes>
              <include>exampleSoap.wsdl</include>
            </schemaIncludes>
            <!-- ソース生成先パッケージ -->
            <generatePackage>exampleSoap.api.client.exampleSoap.dto</generatePackage>
            <!-- ソース生成先ディレクトリ -->
            <generateDirectory>${project.basedir}/src/main/java</generateDirectory>
            <!-- 生成するソースのgenerateコメントにタイムスタンプを設定しないようにする(true:設定しない) -->
            <noFileHeader>true</noFileHeader>
            <!-- 生成後のディレクトリ削除要否(false:削除しない) -->
            <clearOutputDir>false</clearOutputDir>
            <!-- episodeファイル生成要否(false:生成しない) ※デフォルトはtrueなので抑止 -->
            <episode>false</episode>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

```

SOAPクライアントクラスの作成
-
WebServiceGatewaySupportを継承したクラスを作成します。
marshalSendAndReceiveでSOAP通信を実行しています。

リクエストのBeanのインスタンスはObjectFactoryで生成します。ObjectFactoryはWSDLからのソース生成時にBeanと一緒に生成されます。

```java:
@Service
public class ExampleSoapClientService extends WebServiceGatewaySupport {

    @SuppressWarnings("unchecked")
    public ExampleSoapResponse execute() throws WebServiceException {
        // RequestのBean作成
        ObjectFactory objectFactory = new ObjectFactory();
        ExampleSoapRequest reqBean = objectFactory.createExampleSoapRequest();
        reqBean.setUserId("user0001");
        reqBean.setName("サンプル太郎"); 
        reqBean.setAge("25"); 
        // ...省略
        // 作成したリクエストのBeanをJAXBElementにラップする
        JAXBElement<ExampleSoapRequest> request = objectFactory.createExampleSoap(reqBean);

        JAXBElement<ExampleSoapResponse> response = null;
        // SOAP通信実行
        try {
            response = (JAXBElement<ExampleSoapResponse>) getWebServiceTemplate().marshalSendAndReceive(request);
        } catch ( SoapFaultClientException e ) {
            // Faultエラーが返ってきた場合の処理
            // e.getSoapFault().getFaultDetail()でFault電文の<detail>タグ内の情報を取得できる
        } catch ( WebServiceException e ) {
            // その他のSOAP通信エラーが発生した場合の処理
        }
        return response.getValue(); // getValue()でResponseのルート要素のBeanを取得
    }
}
```
※補足
説明と関係無いため省略していますが、クラス内で`@Value`や`@Autowired`を使っているためサービスクラス(`@Service`)として作成しています。
他のコンポーネントをDIする必要がなければこのクラス自体はSpringアノテーションはつけず、後述のConfigクラスでBean定義する形がSpring公式のサンプルにも則っているので綺麗だと思います。

JAXBマーシャラのBean定義とSOAPクライアントクラスの設定
-
JAXBマーシャラをBean定義し、先ほど作成したSOAPクライアントクラスへ設定します。
エンドポイントの設定もここで行っています。

※今回呼出すSOAPサービスはBasic認証があるため下記サンプル内でBasic認証に関する設定もしていますが、認証なしの場合は不要です。
######SOAPクライアントクラスをサービスクラス(`@Service`)で作成した場合

```java:
@Configuration
public class SoapClientConfig {
    
    /** SOAPエンドポイント */
    @Value("${exampleSoap.endpointUri}")
    private String exampleSoapEndpointUri;

    /** Basic認証情報 ※認証なしの場合は不要 */
    @Value("${exampleSoap.basicAuthPassword")
    private String basicAuthPass;
    
    /** SOAPクライアントクラス（WebServiceGatewaySupportのサブクラス） */
    @Autowired
    private ExampleSoapClientService exampleSoapClientService;

    /** JAXBマーシャラのBean定義 */
    @Bean
    public Jaxb2Marshaller exampleSoapMarshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        // SoapのBean格納パッケージを指定
        // 可変長引数なのでアプリケーション内で複数のSOAPサービスを呼出す場合はカンマ区切りで指定
        marshaller.setPackagesToScan("exampleSoap.api.client.exampleSoap.dto");
        
        // SOAPクライアントクラスの設定
        exampleSoapClientService.setMarshaller(marshaller); // マーシャラの設定
        exampleSoapClientService.setUnmarshaller(marshaller); // アンマーシャラの設定
        exampleSoapClientService.setDefaultUri(exampleSoapEndpointUri); // エンドポイントの設定
        // Basic認証をする場合の設定　※認証なしの場合は不要
        exampleSoapClientService.getWebServiceTemplate().setMessageSender(new HttpUrlConnectionMessageSender() {
            @Override
            protected void prepareConnection(HttpURLConnection connection) throws IOException {
                // パスワードをBase64エンコードしてリクエストヘッダに追加する
                String password = basicAuthPass;
                String encodedPass = Base64.getEncoder().encodeToString(password.getBytes());
                connection.setRequestProperty("Authorization", "Basic" + encodedPass);
                super.prepareConnection(connection);
            }
        });
        return marshaller;
    }
}
```

######SOAPクライアントクラスをSpringアノテーションをつけずに作成した場合

```java:
@Configuration
public class SoapClientConfig {
    
    /** SOAPエンドポイント */
    @Value("${exampleSoap.endpointUri}")
    private String exampleSoapEndpointUri;

    /** Basic認証情報 ※認証なしの場合は不要 */
    @Value("${exampleSoap.basicAuthPassword")
    private String basicAuthPass;
    
    /** JAXBマーシャラのBean定義 */
    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        // SoapのBean格納パッケージを指定
        // 可変長引数なのでアプリケーション内で複数のSOAPサービスを呼出す場合はカンマ区切りで指定
        marshaller.setPackagesToScan("exampleSoap.api.client.exampleSoap.dto");        
        return marshaller;
    }

    /** SOAPクライアントクラスのBean定義 */
    @Bean
    public ExampleSoapClientService exampleSoapClientService (Jaxb2Marshaller marshaller) {
        ExampleSoapClientService exampleSoapClientService = new ExampleSoapClientService();
        // SOAPクライアントクラスの設定
        exampleSoapClientService.setMarshaller(marshaller); // マーシャラの設定
        exampleSoapClientService.setUnmarshaller(marshaller); // アンマーシャラの設定
        exampleSoapClientService.setDefaultUri(exampleSoapEndpointUri); // エンドポイントの設定
        // Basic認証をする場合の設定　※認証なしの場合は不要
        exampleSoapClientService.getWebServiceTemplate().setMessageSender(new HttpUrlConnectionMessageSender() {
            @Override
            protected void prepareConnection(HttpURLConnection connection) throws IOException {
                // パスワードをBase64エンコードしてリクエストヘッダに追加する
                String password = basicAuthPass;
                String encodedPass = Base64.getEncoder().encodeToString(password.getBytes());
                connection.setRequestProperty("Authorization", "Basic" + encodedPass);
                super.prepareConnection(connection);
            }
        });
        return exampleSoapClientService;
    }
}
```


リクエスト/レスポンス電文をログ出力する
-
application.ymlに以下を定義することで出力されます。

```yaml:application.yml
logging:
  level:
    org.springframework.ws.client.MessageTracing.sent: TRACE
    org.springframework.ws.client.MessageTracing.received: TRACE
```

参考
-
[Spring公式： SOAP Web サービスの使用](https://spring.pleiades.io/guides/gs/consuming-web-service/)