SOAP API
SOAP APIとは?
XML形式で書かれており、以下のような情報が記述されています。
・リクエスト・レスポンスのデータ型や構造
・アクセス先のURL(エンドポイント)や通信プロトコル
歴史的には『SOAP API』が古く『SOAP』は【Simple Object Access Protocol】の略で、当時はとっても簡単なAPIの位置づけでした。 ただし現在主流の『REST API』に慣れてる人間からすると『超~面倒臭いんですけどっ!!』って感覚です。
『SOAP』に比べれば『REST』は名前の通り【寝ながらでも作れるよ!】って感じですよ。
※ 本来は[Representational State Transfer]の略です 新規プロジェクトで『よし、SOAPサーバを立てよう!』なんて物好きな人はいません。
ただし銀行やレガシーシステムでは『SOAP』が現役で稼働してたりします。
そんなシステムにAPI連携をする要件がきたらどうなりますか? 当然先方から『あ、うちはSOAP通信だから、ヨロシク!』ですよね。 なのでSOAPに関しては『APIクライアント』に限定した内容を解説します。
<!-- Jakarta Web Services API (jakarta.jws や jakarta.xml.ws が含まれます) -->
<dependency>
<groupId>jakarta.xml.ws</groupId>
<artifactId>jakarta.xml.ws-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
WSDLファイル
どうですか? 何やら非常に香ばしい匂いが漂い始めたと思いませんか。
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="UserService"
targetNamespace="http://example.com/userservice"
xmlns:tns="http://example.com/userservice"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<!-- データ型の定義 -->
<types>
<xsd:schema targetNamespace="http://example.com/userservice">
<xsd:element name="GetUserRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userId" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetUserResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userName" type="xsd:string"/>
<xsd:element name="email" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
<!-- 電文(メッセージ)構造の定義 -->
<message name="GetUserRequestMessage">
<part name="parameters" element="tns:GetUserRequest"/>
</message>
<message name="GetUserResponseMessage">
<part name="parameters" element="tns:GetUserResponse"/>
</message>
<!-- インターフェース(操作)の定義 -->
<portType name="UserPortType">
<operation name="GetUser">
<input message="tns:GetUserRequestMessage"/>
<output message="tns:GetUserResponseMessage"/>
</operation>
</portType>
<!-- 通信プロトコルとデータフォーマットのバインディング -->
<binding name="UserBinding" type="tns:UserPortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="GetUser">
<soap:operation soapAction="http://example.com/userservice/GetUser"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<!-- 接続先サービスのエンドポイント定義 -->
<service name="UserService">
<port name="UserPort" binding="tns:UserBinding">
<soap:address location="https://api.example.com/UserService"/>
</port>
</service>
</definitions>
ファイル全体の名前空間(Namespace)を定義し、内部で利用される要素をひとまとめにする役割を持ちます。
コード自動生成ツールなどでクラス名やサービス名の基礎として使われることがあります。
xsd :XML Schema (データ型の定義に使用)
soap:SOAPプロトコルの定義バインディングに使用
サンプルでは、以下の構造となっています。
お約束と思ってください。
リクエスト・レスポンスがあるので、合計2つ作成します。
リクエスト・レスポンスがあるので、合計2つ作成します。
また記述されたURLは通信プロトコルの定義なので【固定文字】となります。
ただし慣習としてURL形式とし、アプリ内のどの関数を処理するかの目安となるアドレスにします。
SOAP APIクライアント作成
src/main/resources/META-INF/wsdl/UserService.wsdl
package dto;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Data;
@Data
@XmlRootElement(name = "GetUserRequest", namespace = "http://example.com/userservice")
@XmlAccessorType(XmlAccessType.FIELD)
public class SoapRequestUserDto {
@XmlElement(required = true, namespace = "http://example.com/userservice")
private String userId;
}
<types>
<xsd:schema targetNamespace="http://example.com/userservice">
<xsd:element name="GetUserRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userId" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
~ 省略 ~
</types>
package dto;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Data;
@Data
@XmlRootElement(name = "GetUserResponse", namespace = "http://example.com/userservice")
@XmlAccessorType(XmlAccessType.FIELD)
public class SoapResponseUserDto {
private String userName;
private String email;
}
<types>
~ 省略 ~
<xsd:element name="GetUserResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userName" type="xsd:string"/>
<xsd:element name="email" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
package api.soap;
import dto.SoapRequestUserDto;
import dto.SoapResponseUserDto;
import jakarta.jws.WebMethod;
import jakarta.jws.WebParam;
import jakarta.jws.WebResult;
import jakarta.jws.WebService;
import jakarta.jws.soap.SOAPBinding;
@WebService(targetNamespace = "http://example.com/userservice", name = "UserPortType")
@SOAPBinding(style = SOAPBinding.Style.DOCUMENT
, use = SOAPBinding.Use.LITERAL, parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface UserPortType {
@WebMethod(operationName = "GetUser", action = "http://example.com/userservice/GetUser")
@WebResult(name = "GetUserResponse", targetNamespace = "http://example.com/userservice", partName = "parameters")
public SoapResponseUserDto getUser(
@WebParam(name = "GetUserRequest", targetNamespace = "http://example.com/userservice", partName = "parameters")
SoapRequestUserDto request);
}
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="UserService"
targetNamespace="http://example.com/userservice"
xmlns:tns="http://example.com/userservice"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<!-- データ型の定義 -->
<types>
<xsd:schema targetNamespace="http://example.com/userservice">
<xsd:element name="GetUserRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userId" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetUserResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userName" type="xsd:string"/>
<xsd:element name="email" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
<!-- 電文(メッセージ)構造の定義 -->
<message name="GetUserRequestMessage">
<part name="parameters" element="tns:GetUserRequest"/>
</message>
<message name="GetUserResponseMessage">
<part name="parameters" element="tns:GetUserResponse"/>
</message>
<!-- インターフェース(操作)の定義 -->
<portType name="UserPortType">
<operation name="GetUser">
<input message="tns:GetUserRequestMessage"/>
<output message="tns:GetUserResponseMessage"/>
</operation>
</portType>
<!-- 通信プロトコルとデータフォーマットのバインディング -->
<binding name="UserBinding" type="tns:UserPortType">">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="GetUser">
<soap:operation soapAction="http://example.com/userservice/GetUser"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<!-- 接続先サービスのエンドポイント定義 -->
<service name="UserService">
<port name="UserPort" binding="tns:UserBinding">
<soap:address location="https://api.example.com/UserService"/>
</port>
</service>
</definitions>
package api.soap;
import java.net.URL;
import javax.xml.namespace.QName;
import dto.SoapRequestUserDto;
import dto.SoapResponseUserDto;
import jakarta.enterprise.context.RequestScoped;
import jakarta.xml.ws.Service;
@RequestScoped
public class SoapUserApiClient {
public void getUser() {
try {
// WSDLのURLとQName(名前空間URI + サービス名)を指定
URL wsdlUrl = SoapUserApiClient.class.getClassLoader()
.getResource("META-INF/wsdl/UserService.wsdl");
if (wsdlUrl == null) {
throw new IllegalArgumentException("WSDLファイルがリソース配下に見つかりません。");
}
QName serviceName = new QName("http://example.com/userservice", "UserService");
// サービスを動的生成
Service service = Service.create(wsdlUrl, serviceName);
// 定義したインターフェースを指定してポートを取得
UserPortType port = service.getPort(UserPortType.class);
// 通信実行
SoapRequestUserDto request = new SoapRequestUserDto();
request.setUserId("12345");
SoapResponseUserDto response = port.getUser(request);
System.out.println("User Name : " + response.getUserName());
System.out.println("Email : " + response.getEmail());
} catch (Exception e) {
e.printStackTrace();
}
}
}
次に『QName』を設定します。
第2引数:service name="UserService">の『name属性名』の値とします。
第2引数:QName
リクエストDTOに値を設定し『port』インスタンスから、対象のメソッド名を指定してAPIリクエストします。
※実際は『WSDL』に定義しているURL「https://api.example.com/UserService」は存在しないのでエラーになります
PHP側にSOAPサーバを立てる
まずは『php.ini』の保存パスを探して下さい。
対象モジュールは以下となりますので『;』が付いている場合は外します。
;extension=soap ※当然apacheは再起動します
ただし今回はPHP側のサービスに本当に接続するため、両『WSDL』ファイルの以下の部分を修正します。
【修正後】<soap:address location="http://localhost/soap/SoapService.php"/>
<operation name="GetUser">
<?php
namespace App;
// リクエスト用 DTO
class GetUserRequest {
public string $userId;
}
// レスポンス用 DTO
class GetUserResponse {
public string $userName;
public string $email;
}
// SOAP サービス本体
class UserService {
// GetUser 操作(メソッド)
public function GetUser(GetUserRequest $request): GetUserResponse
{
$response = new GetUserResponse();
if ($request->userId === '12345') {
$response->userName = '山田 太郎';
$response->email = '[email protected]';
} else {
$response->userName = 'Unknown User';
$response->email = '[email protected]';
}
return $response;
}
}
関数名は『WSDL』ファイルの定義通り『GetUser』とします。
処理内容は単純で、リクエストされた『userId = '12345'』か、それ以外かで返却内容を制御しています。
<?php
// ロジックファイルを読み込む
require_once __DIR__ . '/UserService.php';
use App\UserService;
use App\GetUserRequest;
use App\GetUserResponse;
// 開発時はWSDLのキャッシュをオフにする
ini_set('soap.wsdl_cache_enabled', '0');
// WSDLファイルのパスとオプション(同じ soap フォルダ内にある想定)
$wsdl = __DIR__ . '/UserService.wsdl';
$options = [
'soap_version' => 1,
'encoding' => 'UTF-8',
'classmap' => [
'GetUserRequest' => GetUserRequest::class,
'GetUserResponse' => GetUserResponse::class,
]
];
try {
// SoapServer インスタンス作成
$server = new \SoapServer($wsdl, $options);
// 処理クラスをセット
$server->setClass(UserService::class);
// リクエストの処理を実行(JavaからのPOSTを受けてXMLを返す)
$server->handle();
} catch (\Exception $e) {
http_response_code(500);
echo $e->getMessage();
}
http://localhost/soap/SoapService.php 特にエラーが画面に表示されなければ成功です!
JAVA側からもアクセスしてみて下さい。 今度はエラーにならず以下期待した値が返却されたハズです。
System.out.println("Email : " + response.getEmail());
複雑な返却値
・メールアドレス
では返却も複数(List型)で返却する様にカスタマイズしてみましょう。
<xsd:sequence>
<xsd:element name="userName" type="xsd:string"/>
<xsd:element name="email" type="xsd:string"/>
</xsd:sequence>
<xsd:sequence>
<xsd:element name="userName" type="xsd:string"/>
<xsd:element name="email" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
また『minOccurs="0"』は、空の配列を許容する場合の属性となります。
private String email;
⇒
private List<String> email;
・UserService.php
また、返却で以下配列を定義して、それを返却値とします。
$mails = ['[email protected]', '[email protected]', '[email protected]'];
<?php
namespace App;
// リクエスト用 DTO
class GetUserRequest {
public string $userId;
}
// レスポンス用 DTO
class GetUserResponse {
public string $userName;
public array $email;
}
// SOAP サービス本体
class UserService {
// GetUser 操作(メソッド)
public function GetUser(GetUserRequest $request): GetUserResponse
{
$emails = ['hoge@hoge.com', 'foo@foo.com', 'bar@bar.com'];
$response = new GetUserResponse();
if ($request->userId === '12345') {
$response->userName = '山田 太郎';
$response->email = $emails;
} else {
$response->userName = 'Unknown User';
$response->email = $emails;
}
return $response;
}
}
つまり以下の様な構造です。
<GetUserResponse>
<userName>12345</userName>
<email>[email protected]</email>
<email>[email protected]</email>
<email>[email protected]</email>
<profile>
<tel>09012345678</tel>
<sex>man</sex>
<address>東京都</address>
</profile>
</GetUserResponse>
ただし、中身の定義をするための『マッピング名』を指定し、中身を別の定義で記述します。
<types>
<xsd:schema targetNamespace="http://example.com/userservice"
xmlns:tns="http://example.com/userservice"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
<xsd:element name="GetUserRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userId" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetUserResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="userName" type="xsd:string"/>
<xsd:element name="email" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="profile" type="tns:ProfileItem" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="ProfileItem">
<xsd:sequence>
<xsd:element name="key" type="xsd:string"/>
<xsd:element name="value" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
</types>
<xsd:element name="profile" type="tns:ProfileItem" minOccurs="0" maxOccurs="unbounded"/>
package dto;
import java.util.ArrayList;
import java.util.List;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Data;
@Data
@XmlRootElement(name = "GetUserResponse", namespace = "http://example.com/userservice")
@XmlAccessorType(XmlAccessType.FIELD)
public class SoapResponseUserDto {
private String userName;
private List<String> email;
private List<ProfileItem> profile = new ArrayList<>();
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class ProfileItem {
private String key;
private String value;
}
}
このプロパティは『ProfileItem』型とし、本体はサブクラスとして定義します。
サブクラス内のプロパティは、PHPの返却方法と同様に『key』『value』となります。
WSDLファイルで定義した『ProfileItem』クラスを追加します。
class ProfileItem {
public string $key;
public string $value;
public function __construct(string $key, string $value) {
$this->key = $key;
$this->value = $value;
}
}
public array $profile;
~ 省略 ~
$mails = ['hoge@hoge.com', 'foo@foo.com', 'bar@bar.com'];
$response = new GetUserResponse();
if ($request->userId === '12345') {
$response->userName = '山田 太郎';
$response->email = $mails;
$response->profile = [
new ProfileItem('tel', '09012345678'),
new ProfileItem('sex', 'man'),
new ProfileItem('address', '東京都'),
];
} else {
<?php
namespace App;
// リクエスト用 DTO
class GetUserRequest {
public string $userId;
}
// レスポンス用 DTO
class GetUserResponse {
public string $userName;
public array $email;
public array $profile;
}
// SOAP サービス本体
class UserService {
// GetUser 操作(メソッド)
public function GetUser(GetUserRequest $request): GetUserResponse
{
$emails = ['hoge@hoge.com', 'foo@foo.com', 'bar@bar.com'];
$response = new GetUserResponse();
if ($request->userId === '12345') {
$response->userName = '山田 太郎';
$response->email = $emails;
$response->profile = [
new ProfileItem('tel', '09012345678'),
new ProfileItem('sex', 'man'),
new ProfileItem('address', '東京都'),
];
} else {
$response->userName = 'Unknown User';
$response->email = $emails;
}
return $response;
}
}
// ProfileItemクラス追加
class ProfileItem {
public string $key;
public string $value;
public function __construct(string $key, string $value) {
$this->key = $key;
$this->value = $value;
}
}
$options = [
'soap_version' => 1, // または 1
'encoding' => 'UTF-8',
'classmap' => [
'GetUserRequest' => GetUserRequest::class,
'GetUserResponse' => GetUserResponse::class,
'ProfileItem' => \App\ProfileItem::class,
]
];
request.setUserId("12345");
SoapResponseUserDto response = port.getUser(request);
// そのまま取出せます
System.out.println("User Name : " + response.getUserName());
// 配列として取出す
for (String mail : response.getEmail()) {
System.out.println("Email : " + mail);
}
// 配列として取出すが、値が『key』『value』あります
for (ProfileItem prof : response.getProfile()) {
System.out.println(prof.getKey() + " : " + prof.getValue());
}
どうでしたか? 二度と関わりたくない通信方式なのがご理解できたと思いますw





名前空間は特に『URL』形式にする必要はありません。
よって、このURLは特に何処かに接続確認されている訳ではありません。何故全ての定義にURL形式が使われてるかは、バッティングを防ぐためです。
全世界中にある『WSDL』ファイルを仮に読込んだ時に、同じ名前空間名があると都合が悪いからです。
URL形式であれば、固有のドメイン名があるためバッティングする危険性がありませんよね。
『definitions』タグをまとめると、以下の様な書式となります。
xmlns:tns="【URL①】"
xmlns:xsd="【URL②】"
xmlns:soap="【URL③】"
xmlns="【URL④】">