Enterprise & Integration · سازمانی و یکپارچهسازی سنیورSenior ~70 دقیقه مطالعه~63 min read
SOAP، WSDL و SOA: یکپارچهسازی سازمانیSOAP, WSDL & SOA: Enterprise Integration
از معنای واقعی SOA و قرارداد سرویس تا خواندن خطبهخط WSDL و XSD، ساخت سرویس با Spring-WS و Apache CXF، WS-Security و MTOM، مقایسهی بیرحمانه با REST، الگوهای یکپارچهسازی با Camel و نگاهی کوتاه به ISO 8583 و ISO 20022 — همراه با تلههای واقعی تولید و پاسخهای مصاحبه.From what SOA actually means to reading a WSDL and XSD line by line, building services with Spring-WS and Apache CXF, the WS-* specs banks ask about, a rigorous SOAP-versus-REST comparison, integration patterns with Camel, and a payments primer on ISO 8583 and ISO 20022 — with the production traps and interview answers that go with them.
پیشنیاز:Prerequisites: طراحی API: REST، OpenAPI و نسخهبندیAPI Design: REST, OpenAPI & Versioning
آگهیهای شغلی بانکها، بیمهها و شرکتهای بزرگ کنارِ «REST» تقریباً همیشه یک کلمهی دیگر هم دارند: SOAP. خیلی از مهندسهای نسل جدید ردش میکنند چون «قدیمی است». این اشتباه هم مصاحبه را از دست میدهد، هم باعث میشود روزی که مقابل یک WSDL دوهزارخطی مینشینی ندانی از کجا شروع کنی.
SOAP قدیمی نیست، مستقر است. میلیاردها تراکنش روی همین پروتکل جابهجا میشود، چون در دنیایی ساخته شد که «قرارداد مکتوب، امضای دیجیتال روی بخشی از پیام، و تضمین تحویل» شوخیبردار نبود. این فصل SOAP را از صفر میسازد — نه بهعنوان یادگاری تاریخی، بلکه ابزاری که باید بتوانی فردا صبح در تولید دیباگش کنی.
اول SOA را میفهمیم (سرویس، قرارداد، اتصال سست، حاکمیت) و نسبتش با microservices. بعد SOAP را باز میکنیم: Envelope/Header/Body، fault، تفاوت ۱.۱ و ۱.۲، binding روی HTTP. سپس XSD و WSDL را خطبهخط. بعد وارد جاوا میشویم: contract-first در برابر code-first، تولید کد با wsdl2java/wsimport، و سرویس واقعی با Spring-WS و Apache CXF. بعد WS-* که بانک از آن میپرسد: Security، Addressing، ReliableMessaging، MTOM. بعد SOAP در برابر REST، ESB و الگوهای یکپارچهسازی با Camel، تست و دیباگ، و آخر ISO 8583/20022 تا در مصاحبهی پرداخت گم نشوی.
بخش ۰ — «سرویس» و «قرارداد»
وارد آشپزخانه نمیشوی و نمیدانی داخل قابلمه چه خبر است. فقط منو را میگیری: این غذاها هستند، این قیمتها، و اگر سفارش بدهی این را تحویل میگیری.
منو یک قرارداد است، آشپزخانه یک سرویس، و تو مصرفکننده. رستوران میتواند سرآشپز و اجاق را عوض کند؛ تا وقتی منو ثابت است تو متوجه نمیشوی. این یعنی اتصال سست (loose coupling).
- سرویس: قابلیتی کسبوکاری که از پشت شبکه در دسترس است و مالک مشخصی دارد. «استعلام موجودی» سرویس است؛
AccountRepositoryیک کلاس داخلی است. - قرارداد: توصیف رسمی و ماشینخوانِ عملیات، ورودی/خروجی و خطاها. در SOAP این قرارداد یک فایل WSDL است.
- اتصال سست: مصرفکننده فقط به قرارداد وابسته است، نه به زبان، دیتابیس یا کتابخانههای طرف مقابل.
بخش ۱ — SOA واقعاً چیست؟
یک سازمان بزرگ: Core Banking، CRM، سیستم کارت، وام، گزارشگیری — هرکدام از دههای دیگر، با زبانی دیگر. کسبوکار میگوید «هنگام وام، امتیاز اعتباری را بگیر، موجودی را بخوان، در CRM ثبت کن.»
راه ساده و مرگبار: هر سیستم مستقیم به دیتابیس دیگری وصل شود یا یک اتصال نقطهبهنقطه بسازد. با ۶ سیستم تا ۱۵ اتصال، با ۲۰ سیستم تا ۱۹۰ اتصال. اسمش integration spaghetti است.
نمودار: آشفتگی نقطهبهنقطه در برابر لایهی سرویس — Point-to-point chaos versus a service layer.
flowchart LR
subgraph Before["Point-to-point"]
A1[Core Banking] --- B1[CRM]
A1 --- C1[Cards]
B1 --- C1
C1 --- D1[Loans]
A1 --- D1
end
subgraph After["Service layer"]
A2[Core Banking] --> S[(Service contracts)]
B2[CRM] --> S
C2[Cards] --> S
D2[Loans] --> S
end
SOA (Service-Oriented Architecture) پاسخ معماری به این آشفتگی است: هر قابلیت را پشت یک قرارداد صریح بگذار و فقط از طریق آن حرف بزن. چهار ستون واقعیاش:
۱. مرزبندی صریح: فراخوانی سرویس عبور از یک مرز است؛ گران، ناموفقشدنی و نیازمند سریالسازی. هرگز وانمود نکن فراخوانی راهدور مثل متد محلی است. ۲. قرارداد مشترک، نه کلاس مشترک: بین دو سرویس schema رد و بدل میشود نه فایل JAR. اگر برای صحبت با سرویس دیگر باید کلاس جاوای او را import کنی، اتصال سست نداری. ۳. خودمختاری: هر سرویس مستقل deploy و مقیاس میشود و میتواند خراب شود بدون اینکه بقیه بیفتند. ۴. سازگاری بر پایهی سیاست: «برای صحبت با من باید امضا و timestamp بفرستی» در قالب سیاست ماشینخوان بیان میشود (WS-Policy)، نه در ایمیل به تیم مقابل.
هیچکس نمیتواند «SOA» به تو بفروشد؛ SOA یک سبک معماری است. آنچه فروشندگان میفروشند ESB است، یک محصول میانافزار. بزرگترین شکستهای SOA در دههی ۲۰۰۰ از همین سردرگمی آمد: سازمان یک ESB گران خرید، همهی منطق کسبوکار را داخلش ریخت، و به یک نقطهی شکست مرکزی و غیرقابلتست رسید.
حاکمیت (governance) یعنی قواعدی که تعیین میکند چه کسی سرویس تعریف میکند، namespaceها چه شکلیاند، نسخهها چطور بازنشسته میشوند و SLA چیست. در SOAP کلاسیک یک registry به نام UDDI قرار بود «دفترچهتلفن سرویسها» باشد؛ عملاً مُرد چون کسی سرویس را پویا کشف نمیکند. اما ایدهاش زنده ماند و امروز اسمش API catalog / developer portal است.
وقتی برای افزودن یک فیلد اختیاری به یک XSD باید سه کمیته تشکیل شود و شش هفته صبر کنی، تیمها میانبر میزنند و مستقیم به دیتابیس وصل میشوند. حاکمیتی که رعایتش کندتر از دور زدنش باشد، رعایت نمیشود. قانون سنیور: حاکمیت را خودکار کن (اعتبارسنجی schema در CI، تست سازگاری قرارداد) نه دستی.
SOA در برابر Microservices
| محور | SOA کلاسیک | Microservices |
|---|---|---|
| اندازهی سرویس | بزرگ، همراستا با یک سیستم سازمانی | کوچک، همراستا با یک bounded context |
| مالکیت داده | معمولاً دیتابیس مشترک سازمانی | دیتابیس اختصاصی هر سرویس |
| هوشمندی کجاست | pipe هوشمند (ESB مسیریابی و تبدیل میکند) | endpoint هوشمند، pipe احمق |
| قرارداد | WSDL/XSD | OpenAPI/Protobuf/AsyncAPI |
| پروتکل | SOAP روی HTTP/JMS | HTTP+JSON، gRPC، پیامرسان |
| استقرار | چند سرویس در یک application server | یک کانتینر مستقل بهازای سرویس |
| هدف اصلی | بازاستفاده و یکپارچهسازی سیستمهای موجود | سرعت تحویل و استقلال تیم |
| تراکنش | تلاش برای تراکنش توزیعشده (WS-AT، 2PC) | سازگاری نهایی، Saga |
جزئیات مرزبندی، Saga و ارتباط سرویسها در فصلهای ms-foundations، ms-communication و ms-data است؛ اینجا تکرارشان نمیکنیم.
اول تعریف مشترک: هر دو قابلیت را پشت قرارداد صریح میگذارند و اتصال سست میسازند. بعد سه تفاوت ساختاری: (۱) مالکیت داده — SOA معمولاً دیتابیس سازمانی مشترک دارد، microservices دیتابیسبهازایسرویس؛ (۲) محل هوشمندی — SOA منطق مسیریابی و تبدیل را در ESB میگذارد، microservices در endpoint و pipe را احمق نگه میدارد؛ (۳) واحد استقرار و مالکیت سازمانی — SOA یک تیم یکپارچهسازی مرکزی دارد، microservices تیمهای مستقل. آخر با قضاوت تمام کن: «microservices بدون خودمختاریِ تیمی، فقط یک SOA توزیعشده با تأخیر شبکهی بیشتر است.»
بخش ۲ — SOAP از صفر
متن نامه یک چیز است، اما پاکت هم اطلاعات دارد: گیرنده، «محرمانه»، «فوری»، شمارهی نامه. متصدی پست به متن کاری ندارد؛ فقط پاکت را میخواند و تصمیم میگیرد.
SOAP همین است: یک پاکت (Envelope) استاندارد که داخلش بدنه (Body) — خودِ نامه — و سربرگ (Header) — اطلاعات پردازشی روی پاکت — قرار میگیرد. امضا، توکن امنیتی، شناسهی پیام، آدرس پاسخ، شمارهی ترتیب: همه در Header میروند، نه در Body.
جدا بودن Header از Body یعنی میانراهیها (intermediaries) میتوانند بدون فهمیدن محتوای کسبوکاری روی پیام کار کنند — مسیریابی، لاگ، بررسی امضا. همین «گسترشپذیری خارج از باند» است که SOAP را از یک RPC ساده جدا میکند. (SOAP در ابتدا مخفف «Simple Object Access Protocol» بود؛ از نسخهی ۱.۲ رسماً دیگر مخفف نیست.)
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:acc="http://bank.example/accounts/v1">
<soap:Header>
<acc:CorrelationId soap:mustUnderstand="true"
soap:role="http://www.w3.org/2003/05/soap-envelope/role/next">
7f3a9c22-1e44-4a0b-9a11-88b2c4d5e6f7
</acc:CorrelationId>
</soap:Header>
<soap:Body>
<acc:GetBalanceRequest>
<acc:accountNumber>IR820540102680020817909002</acc:accountNumber>
<acc:currency>IRR</acc:currency>
</acc:GetBalanceRequest>
</soap:Body>
</soap:Envelope>
Envelopeریشهی سند است.Headerاختیاری و شامل صفر یا چند header block.Bodyاجباری و دقیقاً یکی.mustUnderstand: اگرtrueباشد و گیرندهی هدف آن block را نشناسد، باید خطا برگرداند و نباید پیام را پردازش کند.role(در ۱.۲) یاactor(در ۱.۱) هدف block را مشخص میکند. سه URI استاندارد در ۱.۲ با پیشوندhttp://www.w3.org/2003/05/soap-envelope/role/هستند:next(هر گره میانی و گیرندهی نهایی)،none(هیچ گرهای نباید پردازشش کند)،ultimateReceiver. نبودِroleیعنی مقصد نهایی. معادلnextدر ۱.۱ این است:http://schemas.xmlsoap.org/soap/actor/next.
شایعترین خطای عجیب در ادغامهای بینسازمانی: طرف مقابل نسخهی جدید منتشر میکند و یک header block تازه با mustUnderstand="1" میفرستد. استک SOAP تو قبل از رسیدن به کد تو یک fault با کد MustUnderstand میسازد و لاگ اپلیکیشن هیچ ردی ندارد، چون endpoint تو اصلاً فراخوانی نشده. اگر با «fault بدون هیچ لاگی» روبهرو شدی، اول envelope خام را ببین.
نمودار: مسیر پیام از فرستنده تا گیرندهی نهایی با یک گره میانی — A SOAP message path through an intermediary node.
sequenceDiagram
participant S as Initial sender
participant I as Intermediary gateway
participant R as Ultimate receiver
S->>I: Envelope + Header(security, correlation)
Note over I: Processes headers for role "next"<br/>Relays them, never touches Body
I->>R: Envelope + remaining headers
Note over R: Processes ultimateReceiver headers<br/>Executes the Body operation
R-->>I: Response envelope
I-->>S: Response envelope
Fault — مدل خطای SOAP
در REST از کد وضعیت HTTP استفاده میکنی؛ در SOAP خطا داخل خودِ پیام است: یک Fault داخل Body. دلیلش این است که SOAP مستقل از transport طراحی شده — روی JMS یا SMTP اصلاً کد وضعیت HTTP وجود ندارد.
<!-- SOAP 1.1 -->
<soap:Fault xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>soap:Client</faultcode>
<faultstring>Account number is not a valid IBAN</faultstring>
<faultactor>http://bank.example/accounts</faultactor>
<detail>
<acc:ValidationError xmlns:acc="http://bank.example/accounts/v1">
<acc:field>accountNumber</acc:field>
</acc:ValidationError>
</detail>
</soap:Fault>
<!-- SOAP 1.2 -->
<env:Fault xmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:acc="http://bank.example/accounts/v1">
<env:Code>
<env:Value>env:Sender</env:Value>
<env:Subcode><env:Value>acc:InvalidAccount</env:Value></env:Subcode>
</env:Code>
<env:Reason><env:Text xml:lang="en">Not a valid IBAN</env:Text></env:Reason>
<env:Node>http://bank.example/accounts</env:Node>
<env:Detail>
<acc:ValidationError><acc:field>accountNumber</acc:field></acc:ValidationError>
</env:Detail>
</env:Fault>
| موضوع | SOAP 1.1 | SOAP 1.2 |
|---|---|---|
| namespace پاکت | http://schemas.xmlsoap.org/soap/envelope/ |
http://www.w3.org/2003/05/soap-envelope |
| Content-Type | text/xml |
application/soap+xml |
| اعلام عملیات | هدر HTTP جداگانهی SOAPAction |
پارامتر action داخل Content-Type |
| کد خطا | <faultcode> تخت |
<Code><Value> + <Subcode> تودرتو |
| متن خطا | <faultstring> بدون زبان |
<Reason><Text xml:lang="…"> چندزبانه |
| عامل / جزئیات | <faultactor> / <detail> |
<Node>، <Role> / <Detail> |
| کد سمت کلاینت/سرور | Client / Server |
Sender / Receiver |
| کدهای استاندارد | VersionMismatch, MustUnderstand |
بهعلاوهی DataEncodingUnknown |
mustUnderstand |
"1" / "0" |
"true" / "false" |
| هدفگیری هدر | actor |
role |
| وضعیت استاندارد | یادداشت W3C | توصیهی رسمی W3C |
یک هشدار عملی: هرگز stack trace جاوا را داخل <detail> نریز. هم نشت اطلاعات است (نام کلاس، مسیر فایل، نسخهی کتابخانه) و هم قرارداد را میشکند چون هیچجای XSD تعریف نشده؛ detail باید یک نوع تعریفشده در schema تو باشد (فصل appsec-owasp).
اکثریت قاطع سرویسهای سازمانی موجود هنوز SOAP 1.1 هستند، چون WS-I Basic Profile 1.1 — معیار همکاریپذیری دههی ۲۰۰۰ — روی ۱.۱ بنا شده و ابزارهای قدیمی همان را میفهمند. سرویس جدید با دو طرف مدرن: ۱.۲ بهتر است. طرف مقابل قدیمی: ۱.۱ را انتخاب کن و بحث نکن.
SOAP روی HTTP — پیام خام
POST /accounts/v1 HTTP/1.1
Host: soa.bank.example
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://bank.example/accounts/v1/GetBalance"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetBalanceRequest xmlns="http://bank.example/accounts/v1">
<accountNumber>IR820540102680020817909002</accountNumber>
</GetBalanceRequest>
</soap:Body>
</soap:Envelope>
# SOAP 1.1 — SOAPAction هدر جداگانه است و گیومههایش بخشی از مقدارند
curl -sS -X POST 'https://soa.bank.example/accounts/v1' \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "http://bank.example/accounts/v1/GetBalance"' \
--data-binary @request.xml
# SOAP 1.2 — action داخل Content-Type میآید و هدر SOAPAction وجود ندارد
curl -sS -X POST 'https://soa.bank.example/accounts/v1' \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="http://bank.example/accounts/v1/GetBalance"' \
--data-binary @request.xml
خیلی از سرورهای قدیمی اگر SOAPAction را نفرستی یا خالی بفرستی، پیام را رد میکنند یا به عملیات اشتباه مسیریابی میکنند. مقدار درست را از خصیصهی soapAction داخل <soap:operation> در WSDL بردار، و در ۱.۱ گیومهها را حذف نکن.
چرا SOAP «پروتکل» است و REST «سبک»؟ SOAP مشخصات رسمی دارد که ساختار پیام، مدل پردازش، مدل خطا و قوانین گسترش را الزام میکند؛ یک پیام یا معتبر است یا نیست. REST مجموعهای از قیدها است (بیحالتی، رابط یکنواخت، منابع، cacheability)؛ هیچ استانداردی شکل بدنهی خطا را نمیگوید — این خلأ را بعداً با application/problem+json (RFC 9457) پر کردیم. طراحی REST در فصل api-design.
بگو SOAP یک مشخصات رسمی W3C دارد که ساختار پیام (Envelope/Header/Body)، مدل پردازش (نقشها، mustUnderstand)، مدل خطا (Fault) و مکانیزم گسترش را الزام میکند و مستقل از transport تعریف شده — همان پیام روی HTTP، JMS یا SMTP معتبر است. REST یک سبک معماری با شش قید است، نه فرمت پیام؛ هیچ چیزی در REST نمیگوید بدنهی خطا چه شکلی باشد یا احراز هویت کجا بنشیند. نتیجه: SOAP همکاریپذیری را در پروتکل حل میکند و هزینهاش پرحرفی و پیچیدگی است؛ REST آن را به توافق تیمها میسپارد و هزینهاش ناهمگونی است.
بخش ۳ — XSD: زبانی که شکل داده را تعریف میکند
کادر «نام» حداکثر ۵۰ حرف، «کد ملی» دقیقاً ۱۰ رقم، «تلفن» اختیاری، «نوع حساب» فقط یکی از سه گزینه. اگر غلط پر کنی، متصدی همانجا برش میگرداند و به دایرهی حساب نمیفرستد. XSD (XML Schema Definition) همان فرم چاپی است برای XML، و «متصدی» یک اعتبارسنج است که غلطها را قبل از رسیدن به کد تو رد میکند.
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:acc="http://bank.example/accounts/v1"
targetNamespace="http://bank.example/accounts/v1"
elementFormDefault="qualified" version="1.0">
<xsd:simpleType name="Iban"> <!-- نوع پایه با محدودیت -->
<xsd:restriction base="xsd:string">
<xsd:pattern value="[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="Money"> <!-- نوعی که فرزند دارد -->
<xsd:sequence>
<xsd:element name="amount" type="xsd:decimal"/>
<xsd:element name="currency" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="GetBalanceRequest"> <!-- عنصر سطح بالا -->
<xsd:complexType>
<xsd:sequence>
<xsd:element name="accountNumber" type="acc:Iban"/>
<xsd:element name="asOf" type="xsd:date" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetBalanceResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="available" type="acc:Money"/>
<xsd:element name="hold" type="acc:Money" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
نکتهی طراحی: برای مبلغ همیشه xsd:decimal بگذار (نه xsd:float/xsd:double)، و برای شناسهها بهجای xsd:string خام یک pattern تعریف کن تا خطاها در مرز رد شوند نه در منطق.
targetNamespace: این schema عناصرِ کدام فضاینام را تعریف میکند. namespace فقط یک شناسهی یکتای URIمانند است؛ لازم نیست آدرس واقعی باشد.elementFormDefault="qualified": عناصر فرزند هم باید با namespace ظاهر شوند.xsd:sequenceترتیب را الزام میکند،xsd:choiceفقط یکی،xsd:allبدون ترتیب (با محدودیتهای سنگین در XSD 1.0).minOccurs="0"اختیاری،maxOccurs="unbounded"تکرارشونده،nillable="true"یعنی عنصر حاضر ولی باxsi:nil="true"تهی — با «غایب بودن» فرق دارد.xsd:includeschema دیگری با همان namespace را میآورد؛xsd:importیکی با namespace متفاوت. انواع پایهی مشترک سازمان (Money,Address) را در یک namespace جدا بگذار و import کن — اما کوچک نگهش دار، وگرنه هر تغییر کوچک همهی سرویسها را به build مجدد مجبور میکند.
پیشفرضش unqualified است. آنوقت پیام تو عنصر ریشه را با namespace میفرستد ولی <accountNumber> را بدون آن. طرف مقابل که schema را qualified تفسیر کرده، یک fault مبهم «unexpected element» میدهد. قانون عملی: همیشه elementFormDefault="qualified".
برای اعتبارسنجی از داخل جاوا با SchemaFactory یک شیء Schema بساز و ویژگیهای XMLConstants.ACCESS_EXTERNAL_DTD و ACCESS_EXTERNAL_SCHEMA را خالی کن تا parser به شبکه یا فایلسیستم دست نزند.
کامپایل یک XSD بزرگ دهها میلیثانیه طول میکشد. شیء Schema ایمن برای thread است — یک بار بساز و در یک @Bean نگه دار. Validator ایمن نیست — بهازای هر پیام یکی بساز. ساختن دوبارهی Schema برای هر درخواست، کلاسیکترین دلیل سوختن CPU در gatewayهای XML است.
بخش ۴ — WSDL را خط به خط بخوانیم
datasheet میگوید چه ورودی میگیرد، چه خروجی میدهد، سوکتهایش چه استانداردی دارند و کجا نصب میشود. WSDL همان datasheet است، اما ماشینخوان: ابزار از رویش کد تولید میکند. پنج بخشش دقیقاً همین سؤالها را جواب میدهند: چه دادهای (types)، چه پیامی (message)، چه عملیاتی (portType)، با چه پروتکلی (binding)، در چه آدرسی (service).
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:acc="http://bank.example/accounts/v1"
xmlns:tns="http://bank.example/accounts/v1/wsdl"
targetNamespace="http://bank.example/accounts/v1/wsdl" name="AccountService">
<wsdl:types> <!-- 1) انواع داده -->
<xsd:schema targetNamespace="http://bank.example/accounts/v1/wsdl">
<xsd:import namespace="http://bank.example/accounts/v1"
schemaLocation="accounts-v1.xsd"/>
</xsd:schema>
</wsdl:types>
<wsdl:message name="GetBalanceRequest"> <!-- 2) پیام انتزاعی -->
<wsdl:part name="parameters" element="acc:GetBalanceRequest"/>
</wsdl:message>
<wsdl:message name="GetBalanceResponse">
<wsdl:part name="parameters" element="acc:GetBalanceResponse"/>
</wsdl:message>
<wsdl:portType name="AccountPortType"> <!-- 3) رابط انتزاعی -->
<wsdl:operation name="GetBalance">
<wsdl:input message="tns:GetBalanceRequest"/>
<wsdl:output message="tns:GetBalanceResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="AccountSoapBinding" type="tns:AccountPortType"> <!-- 4) پروتکل -->
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="GetBalance">
<soap:operation soapAction="http://bank.example/accounts/v1/GetBalance"/>
<wsdl:input><soap:body use="literal"/></wsdl:input>
<wsdl:output><soap:body use="literal"/></wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="AccountService"> <!-- 5) آدرس واقعی -->
<wsdl:port name="AccountSoapPort" binding="tns:AccountSoapBinding">
<soap:address location="https://soa.bank.example/accounts/v1"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
نمودار: ساختار WSDL از انتزاع تا آدرس واقعی — WSDL structure, from abstract contract down to a concrete address.
flowchart TD
T[types: XSD element definitions] --> M[message: named parts]
M --> P["portType / interface: operations<br/>input, output, fault"]
P --> B["binding: protocol + style + use<br/>soapAction per operation"]
B --> S["service and port<br/>soap:address location"]
سه لایهی بالا انتزاعیاند و دو لایهی پایین ملموس. همان portType میتواند دو binding داشته باشد (یکی SOAP روی HTTP، یکی روی JMS) و همان binding در چند port با آدرسهای مختلف منتشر شود. این همان «جدایی قرارداد از انتقال» است.
دو خصیصه در binding سرنوشت پیام را تعیین میکنند: style (document یا rpc) و use (literal یا encoded).
| ترکیب | Body چه شکلی است | اعتبارسنجی با XSD | وضعیت امروز |
|---|---|---|---|
rpc/encoded |
نام عملیات + پارامترها با xsi:type و ارجاع چندمرجعی |
ممکن نیست | ممنوع در WS-I Basic Profile 1.1 |
rpc/literal |
نام عملیات بهعنوان wrapper، فرزندان بدون namespace | جزئی | نادر |
document/literal bare |
یک عنصر تعریفشده در XSD، بدون wrapper | کامل | عملیات با یک پارامتر |
document/literal wrapped |
عنصری همنام عملیات که پارامترها را میپوشاند | کامل | پیشفرض صنعتی امروز |
wrapped یک قرارداد نامگذاری است نه یک مقدار WSDL: هم مثل RPC نام عملیات را از ریشهی Body میفهمی، هم کل پیام با XSD اعتبارسنجی میشود.
پیام قابل اعتبارسنجی با XSD نیست، گرافهای چندمرجعی (href/id) تولید میشود و هر استکی کمی متفاوت سریال میکند. اگر WSDL طرف مقابل use="encoded" دارد، به یک سیستم پیش از ۲۰۰۵ وصل میشوی؛ وقت بیشتری برای دیباگ سریالسازی بگذار و payloadهای خام را ذخیره کن.
WSDL 1.1 در برابر 2.0: ریشه از definitions به description، portType به interface (با extends)، port به endpoint، و message کلاً حذف شد (مستقیم به عنصر XSD ارجاع میشود)؛ namespace هم http://www.w3.org/ns/wsdl شد. WSDL 2.0 در ۲۰۰۷ توصیهی رسمی W3C شد ولی بازار نپذیرفت — ابزارهای جاوا و داتنت پشتیبانی کامل ندارند و ۹۹٪ آنچه میبینی WSDL 1.1 است.
وقتی WSDL را پویا تولید میکنی، مقدار soap:address location از روی درخواست HTTP ساخته میشود. پشت ingress این آدرس اغلب میشود http://10.0.3.14:8080/... یعنی IP داخلی pod؛ کلاینت به آن وصل میشود و timeout میخورد. راهحل: در Spring-WS setTransformWsdlLocations(true)، در CXF publishedEndpointUrl، و در proxy ارسال و اعمال X-Forwarded-Proto/X-Forwarded-Host.
پنج بخش را نام ببر: types انواع داده را با XSD تعریف میکند؛ message پیامهای انتزاعی را از partها میسازد؛ portType (در ۲.۰ interface) عملیات را با input/output/fault تعریف میکند — این سه انتزاعیاند. binding میگوید عملیات روی چه پروتکلی و با چه style/use منتقل میشود و service/port آدرس واقعی میدهد — این دو ملموساند. ارزش تفکیک را اضافه کن: یک قرارداد انتزاعی میتواند همزمان روی HTTP و JMS منتشر شود بدون تغییر قرارداد.
style تعیین میکند Body چطور ساخته شود و use تعیین میکند سریالسازی چطور انجام شود. در rpc/encoded نام عملیات ریشهی Body است و مقادیر با xsi:type و ارجاع href کدگذاری میشوند؛ نتیجه اینکه پیام با XSD قابل اعتبارسنجی نیست و بین استکها ناسازگاری میدهد — WS-I Basic Profile 1.1 ممنوعش کرد. در document/literal wrapped، Body یک عنصر تعریفشده در XSD است که همنام عملیات است: هم اعتبارسنجی کامل داری، هم میشود از روی ریشهی Body مسیریابی کرد. امروز پیشفرض عملی همین wrapped است.
بخش ۵ — Contract-first در برابر Code-first
نمودار: دو مسیر تولید — Contract-first versus code-first generation paths.
flowchart LR
subgraph CF["Contract-first"]
X[XSD + WSDL] -->|wsdl2java / wsimport| J1[Generated Java stubs]
J1 --> I1[Your implementation]
end
subgraph CoF["Code-first"]
J2["@WebService Java class"] -->|wsgen / runtime| W2["Generated WSDL"]
W2 --> C2[Consumers]
end
دلیلش «تمیزتر بودن» نیست. دلیلش این است که در code-first قرارداد تو تابعی از جزئیات پیادهسازی توست: عوض کردن نام یک فیلد، ارتقای نسخهی JAXB یا تغییر ترتیب متدها میتواند WSDL را عوض کند و کلاینتهای طرف مقابل را بشکند — بدون اینکه هیچ تستی در repo تو قرمز شود. در contract-first، WSDL یک artifact نسخهدار در VCS است و هر تغییرش در code review دیده میشود. استثنا: سرویس کاملاً داخلی و کوتاهعمر که هر دو سرش مال یک تیم است.
مسیر code-first با JAX-WS این است: کلاس را با @WebService(name=…, serviceName=…, targetNamespace=…) علامت بزن، سبک پیام را با @SOAPBinding(style=DOCUMENT, use=LITERAL, parameterStyle=WRAPPED) تثبیت کن، و هر متد را با @WebMethod(operationName=…, action=…)، پارامترها را با @WebParam(name=…, targetNamespace=…) و خروجی را با @WebResult نامگذاری کن. اگر این نامها را صریح ندهی، فریمورک از نام متد و پارامتر جاوا استفاده میکند — یعنی قرارداد تو به نام متغیرهای محلی گره میخورد. از Jakarta EE 9 بستهها از javax.jws به jakarta.jws منتقل شدهاند (فصل java-ee-jakarta).
JAX-WS با JEP 320 از Java SE 11 بیرون رفت، پس این فرمانها روی JDK مدرن وجود ندارند. راه درست امروز: ابزار را بهعنوان وابستگی Maven/Gradle بیاور — org.apache.cxf:cxf-codegen-plugin (هدف wsdl2java، نسخهی ۴.۲.۳) یا com.sun.xml.ws:jaxws-maven-plugin (هدف wsimport، نسخهی ۴.۰.۵، پیادهسازی مرجع Eclipse Metro).
پلاگین CXF را به فاز generate-sources ببند، sourceRoot را روی target/generated-sources/cxf بگذار و در wsdlOptions/wsdlOption/wsdl مسیر فایل WSDL را بده؛ گزینههای اضافه (-p برای نگاشت namespace به بسته، -validate) داخل extraargs میروند. همان کارها از خط فرمان:
wsdl2java -d target/generated-sources -p example.accounts.gen \
-validate src/main/resources/wsdl/accounts-v1.wsdl # کلاینت + انواع داده
wsdl2java -impl -server src/main/resources/wsdl/accounts-v1.wsdl # اسکلت سمت سرور
xjc -d target/generated-sources -p example.accounts.gen accounts-v1.xsd # فقط از XSD
وسوسهانگیز است که خروجی wsdl2java را commit کنی تا build سریع شود. نکن؛ بعد از شش ماه WSDL و کد تولیدشده از هم جدا میافتند و کسی نمیداند کدام درست است. WSDL/XSD را commit کن و کد را در generate-sources بساز.
بگو contract-first، و دلیل را روی مالکیت قرارداد بنا کن: در code-first، WSDL خروجیِ جانبیِ کد است، پس یک refactor بیضرر میتواند قرارداد را عوض کند و کلاینتها را بشکند بدون هیچ سیگنالی در CI. در contract-first، XSD/WSDL یک artifact نسخهدار در repo است و میتوانی تست سازگاری بگذاری. بعد استثنا را ذکر کن: برای سرویس داخلی کوتاهعمر که هر دو سرش مال یک تیم است، code-first سریعتر است. این نشان میدهد قاعده را حفظ نکردهای بلکه فهمیدهای.
بخش ۶ — XML در جاوا: JAXB، کارایی و امنیت
JAXB (Jakarta XML Binding) پل بین XML و اشیای جاواست: marshalling یعنی شیء ← XML و unmarshalling یعنی XML ← شیء. نگاشتی که باید حفظ باشی:
| ساختار XSD | نتیجه در جاوا |
|---|---|
xsd:element سطح بالا |
کلاس با @XmlRootElement یا JAXBElement<T> در ObjectFactory |
xsd:complexType |
کلاس با @XmlType |
minOccurs="0" |
فیلد nullable (نوع wrapper، نه primitive) |
maxOccurs="unbounded" |
List<T> |
xsd:enumeration |
enum جاوا با @XmlEnumValue |
xsd:dateTime |
پیشفرض XMLGregorianCalendar |
xsd:decimal |
BigDecimal |
double نمیتواند ۰.۱ را دقیق نگه دارد و پس از چند جمع، ماندهی حساب اختلاف پیدا میکند — در بانک یعنی مغایرتگیری شبانه. xsd:decimal باید BigDecimal شود؛ اگر ابزار چیز دیگری داد، با <jaxb:javaType> در فایل binding اصلاحش کن. همان فایل جایی است که با یک XmlAdapter میتوانی xsd:date را به java.time.LocalDate ببری بهجای XMLGregorianCalendar دردناک.
ساخت JAXBContext شامل پویش reflection روی کل بسته است و دهها تا صدها میلیثانیه طول میکشد. اگر آن را در متد سرویس بگذاری، زیر بار CPU را میسوزاند. یک بار بساز (static یا @Bean). برعکس، Marshaller/Unmarshaller ایمن برای thread نیستند — هرگز static نکن.
امنیت XML: XXE و بمب موجودیت
XXE (XML External Entity) یعنی مهاجم در ورودی یک موجودیت خارجی تعریف کند و parser تو فایل سرور را بخواند یا به یک آدرس داخلی درخواست بزند:
<!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<GetBalanceRequest xmlns="http://bank.example/accounts/v1">
<accountNumber>&xxe;</accountNumber>
</GetBalanceRequest>
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
XMLReader reader = spf.newSAXParser().getXMLReader();
Object result = CTX.createUnmarshaller()
.unmarshal(new SAXSource(reader, new InputSource(inputStream)));
// اگر StAX استفاده میکنی:
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
این متد داخلاً یک parser با تنظیمات پیشفرض میسازد و پیشفرضها همیشه امن نیستند. همیشه خودت SAXSource یا StAXSource سختشده بساز و این کنترل را در یک @Bean مرکزی ببند، نه در ده endpoint. حملهی دوم را هم فراموش نکن: billion laughs — یک XML ده کیلوبایتی که با موجودیتهای تودرتو به گیگابایتها حافظه باز میشود. ممنوعکردن DOCTYPE هر دو را با هم میبندد (فصل appsec-owasp).
بخش ۷ — Spring Web Services در عمل
Spring-WS عمداً فقط contract-first است و مثل Spring MVC کار میکند، با یک تفاوت: مسیریابی بر اساس ریشهی payload است نه URL. نسخهی 4.1.x روی Spring Framework 6 و Jakarta EE 10 مینشیند؛ نسخهی 5.0.0 (نوامبر ۲۰۲۵) به Spring Framework 7، Spring Security 7 و Jakarta EE 11 (Servlet 6.1) رفته و null-safety کامل با JSpecify دارد. وابستگیها: spring-boot-starter-web-services بهعلاوهی wsdl4j.
نقطهی ورود، یک MessageDispatcherServlet است که روی /ws/* ثبت میشود؛ روی آن حتماً setTransformWsdlLocations(true) بگذار تا آدرس داخل WSDL با میزبان واقعی درخواست بازنویسی شود.
@EnableWs
@Configuration
public class WebServiceConfig implements WsConfigurer {
@Bean(name = "accounts") // نام bean مسیر انتشار را میسازد: /ws/accounts.wsdl
public DefaultWsdl11Definition accountsWsdl() {
DefaultWsdl11Definition wsdl = new DefaultWsdl11Definition();
wsdl.setPortTypeName("AccountPortType");
wsdl.setLocationUri("/ws");
wsdl.setTargetNamespace("http://bank.example/accounts/v1/wsdl");
wsdl.setSchema(new SimpleXsdSchema(new ClassPathResource("xsd/accounts-v1.xsd")));
return wsdl;
}
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
PayloadValidatingInterceptor v = new PayloadValidatingInterceptor();
v.setSchema(new ClassPathResource("xsd/accounts-v1.xsd"));
v.setValidateRequest(true);
v.setValidateResponse(true);
interceptors.add(v);
}
}
@Endpoint
public class AccountEndpoint {
@PayloadRoot(namespace = "http://bank.example/accounts/v1",
localPart = "GetBalanceRequest")
@ResponsePayload
public GetBalanceResponse getBalance(@RequestPayload GetBalanceRequest request) {
return balances.lookup(request.getAccountNumber());
}
}
DefaultWsdl11Definition از روی XSD، WSDL میسازد با یک قرارداد نامگذاری: عنصر با پسوند Request ورودی، همنامش با Response خروجی و با Fault خطا در نظر گرفته میشود. مسیریابی بر اساس namespace + localPart عنصر ریشهی Body است — یعنی میتوانی ده عملیات را روی یک URL منتشر کنی. برای تبدیل استثنا به fault کافی است روی کلاس استثنا @SoapFault(faultCode = FaultCode.CLIENT, faultStringOrReason = "…") بگذاری. سمت کلاینت، WebServiceTemplate را با یک Jaxb2Marshaller و یک HttpComponents5MessageSender (که connectionTimeout و readTimeout میگیرد) بساز و با marshalSendAndReceive فراخوانی کن.
اکثر تیمها فقط ورودی را اعتبارسنجی میکنند. اما اگر روزی کدت یک پاسخ نامعتبر تولید کند (مثلاً null در فیلد اجباری)، کلاینت آن را میگیرد و خطا در سیستم او رخ میدهد — و پشتیبانی طرف مقابل با تو تماس میگیرد. validateResponse این را در سمت خودت به یک خطای قابللاگ تبدیل میکند. هزینهاش چند میلیثانیه است.
بخش ۸ — Apache CXF
CXF هم JAX-WS (SOAP) و هم JAX-RS (REST) را پوشش میدهد و پشتیبانی WS-* عمیقتری دارد. شاخهی پایدار 4.2.x است (4.2.3 در تابستان ۲۰۲۶) با هدف Jakarta EE 11 و JDK 17 به بالا؛ وابستگی org.apache.cxf:cxf-spring-boot-starter-jaxws و ویژگی cxf.path=/services. سمت سرور یک EndpointImpl میسازی و با setAddress("/accounts/v1")، setWsdlLocation("classpath:wsdl/accounts-v1.wsdl") و setPublishedEndpointUrl(...) منتشرش میکنی — همان publishedEndpointUrl که تلهی آدرس داخلی pod در WSDL را میبندد. سمت کلاینت:
JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
factory.setServiceClass(AccountPortType.class);
factory.setAddress("https://soa.bank.example/services/accounts/v1");
AccountPortType port = (AccountPortType) factory.create();
HTTPConduit conduit = (HTTPConduit) ClientProxy.getClient(port).getConduit();
HTTPClientPolicy policy = new HTTPClientPolicy();
policy.setConnectionTimeout(3_000); // میلیثانیه
policy.setReceiveTimeout(10_000);
policy.setAllowChunking(false); // بعضی سرورهای قدیمی chunked را نمیفهمند
conduit.setClient(policy);
پیشفرض بعضی استکهای SOAP «تا ابد منتظر بمان» است. اگر سرویس بالادستی هنگ کند، threadهای سرور تو یکییکی روی read() قفل میشوند تا استخر thread تمام شود و کل اپلیکیشن — حتی بخشهای بیربط — بمیرد. کلاسیکترین شکست آبشاری. همیشه connectionTimeout و receiveTimeout بگذار و اطرافش circuit breaker و bulkhead (فصل resilience).
| معیار | Spring-WS | Apache CXF | JAX-WS RI (Metro) |
|---|---|---|---|
| فلسفه | فقط contract-first | هر دو، contract-first توصیهشده | هر دو، code-first راحتتر |
| مسیریابی | بر اساس ریشهی payload | بر اساس عملیات JAX-WS | بر اساس عملیات JAX-WS |
| پوشش WS-* | Security از طریق WSS4J | کامل: Security, Addressing, RM, Policy | خوب، با افزونههای Metro |
| بهترین کاربرد | سرویس جدید با قرارداد خودت | ادغام سازمانی و WS-* سنگین | سرویس ساده و سریع |
بگو CXF، با دلیل: پوشش کامل WS-SecurityPolicy دارد، یعنی میتوانی سیاست امنیتی را از خود WSDL بخوانی و بگذاری CXF امضا و رمزنگاری را طبقش انجام دهد، بهجای چیدن دستی interceptor؛ ضمناً WS-Addressing و WS-RM را هم دارد. بعد نشان بده تعصب نداری: اگر پروژه فقط یک سرویس تازه با قرارداد خودمان بود، Spring-WS سادهتر و contract-first-محورتر است. آخر یادآوری کن در هر دو حالت timeout، سقف اندازهی پیام و سختسازی XML را باید دستی تنظیم کنی.
بخش ۹ — WS-*: چیزی که بانک از آن میپرسد
«WS-*» خانوادهای از مشخصات است که روی SOAP سوار میشوند و هرکدام یک نگرانی مقطعی را حل میکنند. همهشان در هدر پیام زندگی میکنند، پس مستقل از transport کار میکنند.
۹.۱ WS-Security — امنیت در سطح پیام
TLS امنیت در سطح انتقال میدهد: از A تا B رمز است، اما روی B متن آشکار میشود. اگر پیام از سه gateway عبور کند، در هر سه آشکار میشود. WS-Security امنیت در سطح پیام میدهد: امضا و رمز روی خودِ محتوا مینشیند و تا مقصد نهایی باقی میماند. مبانی رمزنگاری در فصلهای crypto-foundations و pki-tls-gpg.
<soap:Header>
<wsse:Security soap:mustUnderstand="1"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="TS-1">
<wsu:Created>2026-08-13T09:15:00Z</wsu:Created>
<wsu:Expires>2026-08-13T09:20:00Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id="UT-1">
<wsse:Username>loan-service</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">s3cr3t</wsse:Password>
<wsse:Nonce>5uW2rF0z2Q0yQ0uH2QW5wA==</wsse:Nonce>
<wsu:Created>2026-08-13T09:15:00Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
اگر Type را به …#PasswordDigest عوض کنی، مقدار رمز Base64( SHA-1( nonce + created + password ) ) میشود.
دو تله: (۱) برای بازتولید digest، سرور باید رمز را بهصورت متن آشکار یا برگشتپذیر ذخیره کرده باشد — یعنی نمیتوانی با bcrypt/Argon2 هش کنی؛ (۲) الگوریتم SHA-1 است. عملاً digest فقط جلوی شنیدن رمز روی سیم را میگیرد که TLS بهتر انجامش میدهد. توصیهی امروزی: PasswordText روی TLS اجباری، بهعلاوهی Timestamp با پنجرهی کوتاه و کش nonce — یا بهتر، احراز هویت با گواهی کلاینت (mTLS) و امضای X.509.
قدرت واقعی WS-Security امضا و رمزنگاری بخشی از پیام است: «فقط <CardNumber> را رمز کن و کل Body را امضا کن». پیکربندی در CXF با WSS4J (شاخهی جاری 4.0.x):
Map<String, Object> p = new HashMap<>();
p.put(ConfigurationConstants.ACTION, ConfigurationConstants.TIMESTAMP + " "
+ ConfigurationConstants.USERNAME_TOKEN + " " + ConfigurationConstants.SIGNATURE);
p.put(ConfigurationConstants.USER, "loan-service");
p.put(ConfigurationConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
p.put(ConfigurationConstants.PW_CALLBACK_REF, new ClientPasswordCallback());
p.put(ConfigurationConstants.SIGNATURE_USER, "loan-signing-key");
p.put(ConfigurationConstants.SIG_ALGO, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
p.put(ConfigurationConstants.SIG_PROP_FILE, "client-crypto.properties");
p.put(ConfigurationConstants.SIGNATURE_PARTS,
"{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body");
p.put(ConfigurationConstants.TTL_TIMESTAMP, "300");
client.getOutInterceptors().add(new WSS4JOutInterceptor(p));
فایل client-crypto.properties با کلیدهای org.apache.wss4j.crypto.merlin.keystore.type=pkcs12، …keystore.file و …keystore.alias به keystore اشاره میکند. در Spring-WS همان کار با Wss4jSecurityInterceptor و ویژگیهای validationActions="Timestamp UsernameToken Signature"، timestampStrict=true و timeToLive=300 انجام میشود.
keystore.password=changeit در repo یعنی نشت از روز اول؛ رمزها باید از secret manager یا متغیر محیطی بیایند و keystore نباید در image کانتینر bake شود (فصل key-management-secrets). و گواهیها منقضی میشوند: یک هشدار سررسید ۳۰روزه بگذار، وگرنه یک صبح همهی تراکنشها با «signature verification failed» رد میشوند.
نمودار: ترتیب پردازش امنیت پیام در سمت گیرنده — Receiver-side WS-Security processing order.
flowchart TD
A[Raw envelope arrives] --> B{Timestamp valid and fresh?}
B -- no --> F[Fault: InvalidSecurityToken]
B -- yes --> C{Nonce already seen?}
C -- yes --> F
C -- no --> D[Verify signature over signed parts]
D -- invalid --> F
D -- valid --> E[Decrypt encrypted parts]
E --> G[Authenticate UsernameToken or X.509 subject]
G --> H[Dispatch to endpoint]
۹.۲ WS-Addressing
در HTTP، «مقصد» همان URL است و پاسخ روی همان اتصال برمیگردد. اما اگر پیام از JMS برود؟ یا پاسخ باید به آدرس دیگری برود؟ WS-Addressing این اطلاعات را از transport به هدر پیام منتقل میکند. namespace (توصیهی W3C در ۲۰۰۶): http://www.w3.org/2005/08/addressing.
<soap:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:To>https://soa.bank.example/services/accounts/v1</wsa:To>
<wsa:Action>http://bank.example/accounts/v1/GetBalance</wsa:Action>
<wsa:MessageID>urn:uuid:7f3a9c22-1e44-4a0b-9a11-88b2c4d5e6f7</wsa:MessageID>
<wsa:ReplyTo><wsa:Address>https://loans.bank.example/callbacks</wsa:Address></wsa:ReplyTo>
</soap:Header>
در پاسخ، <wsa:RelatesTo> مقدار MessageID درخواست را برمیگرداند — همبستگی استاندارد و ماشینخوان. فعالکردنش در CXF: @Addressing(enabled = true, required = true) روی کلاس پیادهسازی.
نمودار: پاسخ نامتقارن با WS-Addressing — Asynchronous callback correlation with WS-Addressing.
sequenceDiagram
participant C as Consumer
participant P as Provider
participant CB as Callback endpoint
C->>P: Request (MessageID=urn:uuid:A, ReplyTo=CB)
P-->>C: HTTP 202 Accepted (no body)
Note over P: Long-running processing
P->>CB: Response (RelatesTo=urn:uuid:A)
CB-->>P: HTTP 202 Accepted
MessageID را کلید idempotency و شناسهی ردیابی بگیر. اگر کلاینت بهخاطر timeout دوباره بفرستد و MessageID یکسان باشد، سمت سرور تشخیص میدهی و پاسخ کششده را برمیگردانی بهجای اینکه دو بار پول منتقل کنی. همان الگویی که در REST با هدر Idempotency-Key میسازیم — فقط اینجا استاندارد است.
۹.۳ WS-ReliableMessaging
فرستنده و گیرنده یک sequence برقرار میکنند، هر پیام MessageNumber میگیرد و گیرنده SequenceAcknowledgement برمیگرداند؛ اگر ack نیامد، ارسال مجدد. تضمینهای قابل انتخاب: AtMostOnce، AtLeastOnce، ExactlyOnce، InOrder.
نمودار: چرخهی حیات یک sequence — Lifecycle of a WS-RM sequence.
stateDiagram-v2
[*] --> Creating: CreateSequence
Creating --> Active: CreateSequenceResponse
Active --> Active: Message(n) / Acknowledgement
Active --> Retrying: ack timeout
Retrying --> Active: ack received
Retrying --> Faulted: max retries exceeded
Active --> Terminated: TerminateSequence
Faulted --> [*]
Terminated --> [*]
WS-RM قابلیت اطمینان را در حافظهی دو طرف میسازد؛ اگر فرستنده restart شود و ذخیرهی پایدار پیکربندی نشده باشد، sequence از دست میرود. ضمناً «exactly once» فقط در سطح تحویل پیام است نه اثر کسبوکاری: اگر گیرنده پردازش کند و قبل از ارسال ack بمیرد، پیام دوباره میآید. در ۲۰۲۶ اگر واقعاً تحویل مطمئن میخواهی، جواب درست معمولاً یک broker است (فصلهای messaging و rabbitmq-amqp) بهعلاوهی outbox و مصرفکنندهی idempotent.
۹.۴ MTOM — ضمیمه بدون تورم Base64
یک PDF یکمگابایتی داخل XML باید base64 شود: حدود ۳۳٪ بزرگتر و کل رشته در حافظه. MTOM با کمک XOP بایتها را بیرون میکشد و بهعنوان یک بخش MIME جدا میفرستد؛ در XML فقط یک اشارهگر میماند و کل پیام یک multipart/related; type="application/xop+xml" میشود:
<content>
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
href="cid:statement@example"/>
</content>
بخش MIME متناظر یک هدر Content-ID: <statement@example> دارد و بایتهای خام PDF را حمل میکند. در جاوا کافی است عنصر در XSD از نوع xsd:base64Binary با xmime:expectedContentTypes باشد (به DataHandler نگاشت میشود) و روی پیادهسازی @MTOM(enabled = true, threshold = 4096) بگذاری.
زیر ~۱۰۰ کیلوبایت: base64 داخل پیام، بدون دردسر MIME. از ۱۰۰ کیلوبایت تا چند مگابایت: MTOM. بالای چند مگابایت: فایل را اصلاً از SOAP رد نکن — در object storage بگذار و در پیام فقط یک URL امضاشدهی موقت بفرست. هم حافظهی سرور را نجات میدهد، هم retry را ارزان میکند، هم ازسرگیری آپلود میدهد.
تفاوت را با «سطح» توضیح بده: TLS امنیت در سطح انتقال است و فقط بین دو نقطهی اتصال معتبر؛ روی هر gateway که TLS خاتمه یابد پیام آشکار میشود. WS-Security امنیت در سطح پیام است و تا مقصد نهایی حفظ میشود، حتی از روی JMS. سه قابلیتی که TLS ندارد: (۱) امضای بخشی از پیام و رمز بخشی دیگر — مثلاً فقط شماره کارت رمز شود و بقیه برای مسیریابی خوانا بماند؛ (۲) عدم انکار — امضای دیجیتال ذخیرهشدنی است و بعداً میشود اثبات کرد چه کسی چه فرستاده؛ (۳) استقلال از transport. آخر بگو در عمل هر دو را با هم میگذاریم، نه یکی بهجای دیگری.
سه لایه: (۱) wsu:Timestamp با پنجرهی کوتاه (مثلاً ۵ دقیقه) و ساعت همگام با NTP؛ (۲) کش nonce — هر wsse:Nonce یا wsa:MessageID را تا انتهای پنجره در یک ذخیرهی مشترک (مثلاً Redis با TTL) نگه دار و تکراریها را رد کن؛ در محیط چند-instance این کش باید مشترک باشد وگرنه مهاجم فقط به instance دیگری میزند؛ (۳) امضای دیجیتال روی Timestamp و Body تا مهاجم نتواند زمان را عوض کند. برای امتیاز اضافه بگو که برای عملیات مالی، idempotency در سطح کسبوکار هم لازم است: کلید یکتای تراکنش با قید unique در دیتابیس.
بخش ۱۰ — SOAP در برابر REST
| محور | SOAP | REST |
|---|---|---|
| ماهیت | پروتکل با مشخصات رسمی | سبک معماری با مجموعه قیدها |
| قرارداد | WSDL + XSD، اجباری و ماشینخوان | OpenAPI، اختیاری و توصیفی |
| فرمت پیام | فقط XML | JSON (معمول)، XML، هر چیزی |
| انتقال | HTTP، JMS، SMTP | عملاً فقط HTTP |
| مدل خطا | Fault استاندارد داخل پیام |
کد وضعیت HTTP + بدنهی دلخواه (RFC 9457) |
| امنیت | WS-Security در سطح پیام + TLS | TLS + OAuth2/JWT در سطح انتقال |
| تراکنش | WS-AT (دوفازی، بهندرت) | ندارد؛ Saga در سطح اپلیکیشن |
| نسخهبندی | namespace جدید در XSD | مسیر /v2، هدر، media type |
| کشینگ | عملاً هیچ (همهچیز POST) | بومیِ HTTP (ETag، Cache-Control) |
| اندازه و ابزار | پیام سنگین، تولید کد بسیار بالغ | پیام سبک، تولید کد از OpenAPI |
| بهترین کاربرد | ادغام بینسازمانی، بانکی، دولتی | API عمومی، موبایل، microservices داخلی |
چه وقت یک بانک هنوز SOAP انتخاب میکند؟ (۱) الزام شریک یا نهاد ناظر — طرف مقابل WSDL میدهد و مذاکرهپذیر نیست؛ (۲) قرارداد اجراییشدنی — XSDای که پیام نامعتبر را همان دم رد میکند؛ (۳) امضای دیجیتال و عدم انکار — وقتی باید سالها بعد اثبات کنی پیام از چه کسی آمده و تغییر نکرده؛ (۴) سرمایهگذاری موجود — صدها سرویس مستقر، gateway، مانیتورینگ و runbook.
در عمل تقریباً هیچوقت «SOAP یا REST» نیست، «SOAP و REST» است: یا یک REST facade روی SOAP میسازی (اپلیکیشن موبایل نباید XML بفهمد)، یا یک SOAP facade روی REST (سرویسهای جدیدت RESTاند ولی شریک قدیمی فقط SOAP میفهمد). در هر دو حالت آداپتور باید فقط ترجمه کند، نه اینکه منطق کسبوکار در آن انباشته شود.
برای یک API جدید در ۲۰۲۶، REST یا gRPC (فصل grpc-protobuf) تقریباً همیشه درست است. اما پاسخ «SOAP بد است، همه را migrate کنید» در مصاحبهی سنیور امتیاز منفی دارد، چون نشان میدهد هزینهی migration، ریسک تجاری و تعهدات قراردادی را نمیبینی. جواب بالغ: مرز جدید را مدرن بساز، قدیمیها را پشت facade بگذار، و هنگام جایگزینی قرارداد قدیمی را نگه دار — سرویس جدید همان WSDL را منتشر کند، ترافیک تدریجی منتقل شود و پاسخها در سایه مقایسه شوند. اشتباه رایج: همزمان عوضکردن پیادهسازی و قرارداد، و بعد ندانستن اینکه کدام تغییر خطا ساخته.
بخش ۱۱ — ESB و الگوهای یکپارچهسازی
نامهها با فرمتهای مختلف میآیند. مرکز پستی روی پاکت را میخواند، تصمیم میگیرد به کدام شهر برود، و اگر لازم باشد بستهبندی را عوض میکند؛ نامههای بیآدرس به قفسهی «نامههای مرده» میروند. ESB (Enterprise Service Bus) همین است برای پیامهای نرمافزاری: لایهای میانی که مسیریابی، تبدیل فرمت، تطبیق پروتکل و رسیدگی به خطا را انجام میدهد تا سیستمها لازم نباشد از هم بدانند.
| الگو (EIP) | یکخطی | نمونهی بانکی |
|---|---|---|
| Content-Based Router | بر اساس محتوا مقصد را انتخاب کن | داخلی به Core، بینبانکی به gateway |
| Message Translator | فرمت A را به B تبدیل کن | ISO 8583 به ISO 20022 XML |
| Splitter / Aggregator | پیام مرکب را بشکن / پاسخها را جمع کن | فایل دستهای حقوق ← هر ردیف یک انتقال |
| Normalizer | فرمتهای مختلف را متعارف کن | سه فرمت شعبه ← یک canonical schema |
| Wire Tap | یک کپی برای مانیتورینگ بردار | نسخهی پیام به سامانهی ممیزی |
| Dead Letter Channel | پیام غیرقابلپردازش را جدا کن | صف payments.dlq |
| Idempotent Consumer | پیام تکراری را دوباره اثر نده | کلید یکتای تراکنش |
| Claim Check | دادهی حجیم را بیرون بگذار | PDF در object storage، URL در پیام |
مدل دادهی متعارف (canonical data model): بهجای N×(N−1) ترجمه، یک زبان مشترک تعریف کن تا هر سیستم فقط از فرمت خودش به متعارف و برعکس ترجمه شود — از N² به 2N.
تلهی کلاسیک: ساختن یک Customer متعارف که همهی نیازهای همهی سیستمها را پوشش دهد. نتیجه یک complexType با ۲۰۰ فیلد است که ۱۹۰تایش همیشه خالیاند و هر تغییر کوچکش باید در کمیته تصویب شود. راه درست: مدل متعارف را کوچک و در سطح یک دامنه نگه دار (یک canonical برای «پرداخت»، نه یکی برای کل بانک) — همان بحث bounded context در فصل ddd.
Apache Camel همان الگوها را به شکل کد پیاده میکند؛ شاخهی LTS جاری 4.22.x است (آگوست ۲۰۲۶).
public class PaymentRoute extends RouteBuilder {
@Override
public void configure() {
errorHandler(deadLetterChannel("jms:queue:payments.dlq")
.maximumRedeliveries(3).redeliveryDelay(2000).backOffMultiplier(2));
from("cxf:bean:paymentSoapEndpoint") // ورودی SOAP
.idempotentConsumer(simple("${body.transactionId}"),
MemoryIdempotentRepository.memoryIdempotentRepository(10_000))
.wireTap("jms:queue:audit.copy") // Wire Tap
.bean(PaymentNormaliser.class, "toCanonical") // Message Translator
.choice() // Content-Based Router
.when(simple("${body.channel} == 'INTERNAL'")).to("jms:queue:core.transfer")
.when(simple("${body.amount} > 500000000")).to("jms:queue:aml.review")
.otherwise().to("cxf:bean:interbankSoapClient")
.end();
}
}
نمودار: یک مسیر یکپارچهسازی واقعی — A realistic integration route composed of several EIPs.
flowchart LR
A[SOAP endpoint] --> B[Idempotent consumer]
B --> C[Wire tap to audit]
B --> D[Translator: legacy to canonical]
D --> E{Content-based router}
E -->|internal| F[Core transfer queue]
E -->|high value| G[AML review queue]
E -->|other| H[Interbank SOAP client]
H -->|error after retries| I[(Dead letter queue)]
ESBهای سنتی سه مشکل ساختاری داشتند: گلوگاه و نقطهی شکست مرکزی (یک مسیر بد، کل bus را میخواباند)؛ مالکیت مبهم (منطق کسبوکار در کنسول گرافیکی ESB زندگی میکرد، بدون code review و تست خودکار)؛ و گلوگاه سازمانی (برای هر تغییر کوچک در صف تیم یکپارچهسازی میماندی). پاسخ صنعت: «smart endpoints, dumb pipes». این بهمعنای مرگ الگوها نیست — Camel هنوز پرکاربرد است، فقط بهجای یک محصول مرکزی، یک کتابخانه داخل سرویس توست.
یکشبه حذفش نکن: (۱) مسیرها را از کنسول گرافیکی به کد نسخهدار منتقل کن؛ (۲) مسیرهای هر دامنه را به مالک همان دامنه بده؛ (۳) هر بار که یک مسیر تغییر میکند، همان موقع از bus به سرویس منتقلش کن (strangler). سه سال طول میکشد ولی ریسکش قابل کنترل است، برخلاف بازنویسی بزرگ.
بخش ۱۲ — تست، دیباگ و عملیات
SoapUI (نسخهی متنباز، شاخهی 5.x) هنوز سریعترین راه کار با یک WSDL ناشناخته است: با New SOAP Project آدرس WSDL را بده تا برای هر عملیات یک درخواست نمونه بسازد؛ در Auth گزینهی WSS-Username Token یا WSS Signature را تنظیم کن؛ تب Raw پیام دقیق روی سیم را نشان میدهد و ۹۰٪ دیباگها را حل میکند؛ و با MockService از روی همان WSDL یک سرویس تقلیدی بساز و در CI بالا بیاور — اگر روزی طرف مقابل WSDL را عوض کند، mock دیگر با کد تو نمیخواند و قبل از تولید میفهمی (جایگزینهای سبکتر: WireMock یا Testcontainers، فصل testing). در Postman باید دستی بسازی: POST، بدنهی raw → XML و هدرهای Content-Type و SOAPAction.
curl -v --http1.1 -X POST 'https://soa.bank.example/services/accounts/v1' \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "http://bank.example/accounts/v1/GetBalance"' \
--cert client.pem --key client.key --cacert ca.pem \
--data-binary @request.xml | xmllint --format -
xmllint --noout --schema accounts-v1.xsd response-body.xml # اعتبارسنجی پاسخ ذخیرهشده
الگوی رایج و خطرناک: تیم برای دیباگ LoggingInInterceptor را روشن میکند و فراموش میکند خاموشش کند. حالا شماره کارت، کد ملی و حتی wsse:Password در فایل لاگ و بعد در سامانهی مرکزی لاگ نشستهاند — جایی که دهها نفر دسترسی دارند و backup هم میشود. در تولید فقط متادیتا لاگ کن؛ اگر بدنه لازم است در CXF از LoggingFeature با setLimit(8192)، setLogBinary(false) و setSensitiveElementNames(...) استفاده کن (فصل logging).
جدول ممیزی معمول — رد پیامها بدون ذخیرهی محتوای حساس:
CREATE TABLE soap_audit (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
correlation text NOT NULL,
operation text NOT NULL,
fault_code text,
duration_ms integer,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_fault ON soap_audit (created_at DESC) WHERE fault_code IS NOT NULL;CREATE TABLE soap_audit (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
correlation VARCHAR2(64) NOT NULL,
operation VARCHAR2(128) NOT NULL,
fault_code VARCHAR2(128),
duration_ms NUMBER(10),
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL
);
-- Oracle ایندکس جزئی ندارد؛ ایندکس تابعی زیر سطرهای بدون fault را ذخیره نمیکند
CREATE INDEX idx_audit_fault
ON soap_audit (CASE WHEN fault_code IS NULL THEN NULL ELSE created_at END);PostgreSQL ایندکس جزئی (WHERE) دارد و میتوانی بگویی «فقط سطرهای دارای fault را ایندکس کن». Oracle این را ندارد، اما ایندکس B-tree در Oracle سطرهایی که همهی کلیدهایشان NULL است را ذخیره نمیکند؛ پس یک ایندکس تابعی با CASE همان اثر را میدهد (فصل oracle-postgres-dialects).
| علامت | معنی محتمل | اولین کار |
|---|---|---|
MustUnderstand fault |
هدری با mustUnderstand="1" را نمیشناسی |
envelope خام را ببین |
VersionMismatch |
namespace پاکت اشتباه است | binding و Content-Type را چک کن |
Client/Sender بدون جزئیات |
پیام با XSD نمیخواند | با xmllint --schema محلی اعتبارسنجی کن |
| HTTP 415 | Content-Type غلط |
text/xml در برابر application/soap+xml |
Unexpected element |
مشکل namespace فرزندان | elementFormDefault را ببین |
Invalid timestamp |
اختلاف ساعت | NTP روی هر دو سرور |
Signature verification failed |
c14n، گواهی، یا دستکاری واسطه | مطمئن شو کسی XML را reformat نمیکند |
امضای XML روی شکل کانونیشده (canonicalized) سند محاسبه میشود. اگر یک gateway یا logger پیام را pretty-print کند، فاصله اضافه شود یا ترتیب اعلان namespaceها عوض شود، امضا میشکند و طرف مقابل «signature verification failed» میدهد — درحالیکه کسی به محتوا دست نزده. پیام امضاشده باید بایتبهبایت دستنخورده منتقل شود.
برای مانیتورینگ (فصل observability) اینها را بده: نرخ درخواست بهازای عملیات، صدک ۹۵ و ۹۹ زمان پاسخ، نرخ fault به تفکیک کد (جهش در Sender یعنی کلاینتی خراب شده، جهش در Receiver یعنی تو خراب شدهای)، و روزهای مانده تا انقضای گواهی امضا. سقفهای سختسازی را هم بگذار: در CXF ویژگیهای org.apache.cxf.stax.maxChildElements، maxElementDepth، maxTextLength و attachment-max-size جلوی منع سرویس با payload بزرگ را میگیرند.
بخش ۱۳ — پیامهای مالی: ISO 8583 و ISO 20022
ISO 8583 استاندارد پیام تراکنش کارت است (خودپرداز، POS، سوییچ). برخلاف SOAP متنی و tag-based نیست؛ باینری و موقعیتی است. سه بخش دارد: MTI (چهار رقم که نسخه، کلاس، عملکرد و مبدأ را کد میکند — 0100 درخواست مجوز و 0110 پاسخش، 0200 درخواست مالی، 0400 برگشت)؛ Bitmap (نقشهی ۶۴بیتی که میگوید کدام فیلدها حاضرند؛ روشنبودن بیت ۱ یعنی یک bitmap دوم هم هست)؛ و Data Elements.
| شماره | نام | توضیح |
|---|---|---|
| DE 2 | Primary Account Number | شماره کارت (PAN) |
| DE 4 | Transaction Amount | مبلغ، بدون نقطهی اعشار |
| DE 11 | System Trace Audit Number | STAN — شمارهی پیگیری |
| DE 37 | Retrieval Reference Number | RRN — کلید همبستگی |
| DE 39 | Response Code | 00 یعنی موفق |
| DE 49 | Currency Code | تعیینکنندهی تعداد ارقام اعشار |
| DE 55 | ICC Data | دادهی EMV کارت تراشهدار |
DE 4 یک عدد صحیح با طول ثابت است و تعداد ارقام اعشار از کد ارز (DE 49) میآید. یعنی 000000012345 میتواند ۱۲۳.۴۵ دلار باشد یا ۱۲۳۴۵ ریال. اگر این را در ترجمهات درست نگیری، مبلغها صد برابر میشوند — و در تست دیده نمیشود چون تست را با یک ارز نوشتهای.
ISO 20022 استاندارد پیام مالی مبتنی بر XML است که جای بیتمپ موقعیتی، فیلدهای نامدار و ساختاریافته میگذارد. نام پیامها الگو دارد: pacs.008 (انتقال وجه مشتری)، pacs.002 (گزارش وضعیت)، pain.001 (دستور پرداخت)، camt.053 (صورتحساب).
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.10">
<FIToFICstmrCdtTrf>
<GrpHdr><MsgId>MSG-20260813-0001</MsgId><NbOfTxs>1</NbOfTxs></GrpHdr>
<CdtTrfTxInf>
<PmtId><EndToEndId>E2E-778812</EndToEndId></PmtId>
<IntrBkSttlmAmt Ccy="EUR">1250.00</IntrBkSttlmAmt>
<Dbtr><Nm>Sender Name</Nm></Dbtr>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
ISO 20022 فقط schema است، نه transport: این XML اغلب داخل یک پیام SOAP، روی یک صف، یا در یک فایل دستهای منتقل میشود. یعنی مهارتهای همین فصل — XSD، namespace، اعتبارسنجی، JAXB، امضای XML — دقیقاً همان چیزی است که در یک پروژهی ISO 20022 لازم داری.
«ISO 8583 برای دنیای کارت و مجوزدهی بلادرنگ ساخته شد: فشرده، موقعیتی و بهینه برای پهنای باند کم. ISO 20022 برای دنیای غنی از داده ساخته شد: XML ساختاریافته با فیلدهای نامدار که اطلاعات هویتی و remittance را کامل حمل میکند و برای انطباق و ضدپولشویی حیاتی است. مؤسسات در دورهی گذار هر دو ریل را همزمان اجرا میکنند، و بزرگترین چالش مهندسی ترجمهی بدون افت داده بین این دو است — چون ۸۵۸۳ فیلدهایی را که ۲۰۰۲۲ میخواهد اصلاً ندارد.»
بخش ۱۴ — برگهی تقلب
| چیزی که لازم داری | مقدار / فرمان |
|---|---|
| namespace پاکت ۱.۱ / ۱.۲ | http://schemas.xmlsoap.org/soap/envelope/ · http://www.w3.org/2003/05/soap-envelope |
| Content-Type ۱.۱ / ۱.۲ | text/xml + هدر SOAPAction · application/soap+xml; action="…" |
| namespace WSDL 1.1 / binding | http://schemas.xmlsoap.org/wsdl/ · …/wsdl/soap/ |
| namespace XSD / WS-Addressing | http://www.w3.org/2001/XMLSchema · http://www.w3.org/2005/08/addressing |
namespace wsse / XOP |
…oasis-200401-wss-wssecurity-secext-1.0.xsd · http://www.w3.org/2004/08/xop/include |
| transport استاندارد در WSDL | http://schemas.xmlsoap.org/soap/http |
| تولید کد از WSDL (CXF) | wsdl2java -d out -p com.example.gen -validate service.wsdl |
| تولید اسکلت سرور / از XSD | wsdl2java -impl -server s.wsdl · xjc -d out -p com.example.gen s.xsd |
| زیباسازی / اعتبارسنجی XML | xmllint --format f.xml · xmllint --noout --schema s.xsd d.xml |
| پلاگین Maven: CXF / Metro | cxf-codegen-plugin:wsdl2java · jaxws-maven-plugin:wsimport |
| starter اسپرینگ برای SOAP | spring-boot-starter-web-services + wsdl4j |
بخش ۱۵ — سؤالات مصاحبهی باقیمانده
نسخه در namespace مینشیند نه در URL: http://bank.example/accounts/v1 و .../v2. چون مسیریابی بر اساس عنصر ریشهی Body است، دو namespace یعنی دو قرارداد مجزا که همزمان روی یک endpoint زندگی میکنند. قاعدهی سازگاری: افزودن یک عنصر اختیاری در انتهای xsd:sequence سازگار عقبرو است؛ اما تغییر نوع، اجباریکردن فیلد، تغییر نام یا عوضکردن ترتیب شکننده است. یک نکتهی ظریف که امتیاز میگیرد: افزودن مقدار جدید به یک xsd:enumeration از دید فرستنده بیضرر است اما گیرندهی قدیمی پیام را در اعتبارسنجی رد میکند؛ برای فیلدهایی که رشد میکنند بهجای enumeration از رشتهی با الگو استفاده کن.
اول تفکیک کن کندی در شبکه، پارس XML یا منطق کسبوکار است — زمان را در سه نقطه اندازه بگیر. بعد به مظنونهای خاص SOAP برو: (۱) JAXBContext که در هر درخواست ساخته میشود؛ (۲) اعتبارسنجی که schema را هر بار کامپایل میکند؛ (۳) payloadهای بزرگ base64 که باید MTOM یا claim-check شوند؛ (۴) DOM بهجای StAX برای پیامهای بزرگ؛ (۵) نبودِ connection pool یا خاموشبودن keep-alive. اگر امضای دیجیتال داری، تأیید امضا خودش CPU میخورد. و تأکید کن: اول اندازهگیری، بعد تغییر.
چون «چه کاری» در بدنهی پیام کد شده نه در متد HTTP؛ HTTP فقط تونل انتقال است. هزینهاش از دسترفتن هرچیزی است که HTTP رایگان میدهد: کشینگ (POST قابل کش نیست، پس هیچ CDN یا proxy پاسخ خواندنی را کش نمیکند)، معناشناسی idempotency (proxy نمیداند retry امن است یا نه)، و مشاهدهپذیری ساده (در لاگ گیتوی همهی درخواستها یک URL و یک متد دارند). راهحل: نام عملیات را از SOAPAction یا ریشهی Body بیرون بکش و برچسب متریک کن، و کش را در سطح اپلیکیشن پیاده کن (فصل caching).
لایهبهلایه. شبکه: فقط TLS، ترجیحاً mTLS با allow-list گواهیها. پیام: wsse:Security با Timestamp کوتاه، امضای Body و Timestamp، رمزنگاری فیلدهای حساس، و کش nonce مشترک. پارسر: DOCTYPE ممنوع، موجودیت خارجی خاموش، سقف عمق و تعداد عناصر و طول متن و اندازهی ضمیمه — یعنی بستن XXE و بمب موجودیت. اعتبارسنجی: هر پیام با XSD قبل از رسیدن به منطق. مجوزدهی: نگاشت هویت به نقش و اعمال آن در سطح عملیات، نه فقط endpoint. عملیات: rate limit، timeout، circuit breaker و لاگ متادیتای ماسکشده. آخر بگو همهی اینها را در یک لایهی مشترک بگذار نه در هر endpoint.
SOA یک سبک معماری است، نه محصول: قابلیت را پشت قرارداد صریح بگذار، مرزها را جدی بگیر، schema را به اشتراک بگذار نه کلاس را. microservices همان اصول است با تأکید افراطی بر خودمختاری تیم و حذف pipe هوشمند مرکزی.
SOAP یک پروتکل پیام است: Envelope با Header (فرادادهی پردازشی، mustUnderstand، role) و Body (محتوا یا Fault)؛ ۱.۱ با text/xml و هدر SOAPAction، ۱.۲ با application/soap+xml و پارامتر action. WSDL قرارداد است: types/message/portType انتزاعی، binding/service ملموس؛ document/literal wrapped پیشفرض عملی است. در XSD یادت باشد elementFormDefault="qualified".
در جاوا: contract-first برای هر ادغام بینسازمانی؛ تولید کد با wsdl2java یا wsimport (که از JDK 11 دیگر در JDK نیست)؛ Spring-WS برای سرویس تازه و Apache CXF برای WS-* سنگین. JAXBContext را یک بار بساز، Marshaller را هرگز به اشتراک نگذار، و parser را در برابر XXE سخت کن.
از WS-*: WS-Security امنیت در سطح پیام میدهد که TLS نمیدهد (امضای بخشی، عدم انکار، استقلال از transport)؛ WS-Addressing آدرس و همبستگی را از transport جدا میکند و MessageID کلید idempotency خوبی است؛ WS-RM جایگزین broker نیست؛ MTOM ضمیمه را از تورم base64 نجات میدهد.
و آخر: API جدید را SOAP نساز، ولی SOAP سالم را هم بیدلیل بازنویسی نکن — پشت facade بگذار و تدریجی جایگزین کن. ESB را به god-object تبدیل نکن؛ الگوهای EIP را در کد نسخهدار پیاده کن. و اگر وارد دنیای پرداخت شدی: ISO 8583 موقعیتی و فشرده است و ISO 20022 ساختاریافته و XML — ترجمهی بدون افت داده بین این دو، شغل تماموقت خیلی از مهندسهای بانکی است.
Open the job ads at banks, insurers, telcos and large enterprises and next to "REST" you will almost always find one more word: SOAP. Many newer engineers wave it away as "legacy". That mistake loses interviews — and worse, it means that the day you sit down in front of a two-thousand-line WSDL you will not know where to start.
Here is the truth: SOAP is not old, it is installed. Billions of transactions a day move over this protocol, because it was built for a world where "a written contract, a digital signature over part of the message, and guaranteed delivery" were not optional. This chapter builds SOAP from zero — not as a museum piece, but as a tool you must be able to debug in production tomorrow morning.
First we work out what SOA actually means (service, contract, loose coupling, governance) and how it relates to microservices. Then we open SOAP: Envelope/Header/Body, faults, 1.1 vs 1.2, the HTTP binding. Then we read XSD and WSDL line by line. Then into Java: contract-first vs code-first, generating code with wsdl2java/wsimport, and building real services with Spring-WS and Apache CXF. Then the WS-* specs a bank will ask about: Security, Addressing, ReliableMessaging, MTOM. Then a rigorous SOAP vs REST comparison, ESB and integration patterns with Camel, testing and debugging, and finally ISO 8583 / ISO 20022 so you are not lost in a payments interview.
Part 0 — Two words to nail down first: "service" and "contract"
You never walk into the kitchen, never tell the chef to dice the onion finer, and have no idea what is in the pot. You just get the menu: these are the dishes, this is the price, order one and this is what arrives.
The menu is a contract. The kitchen is a service. You are the consumer. The restaurant can swap the chef, the stove, even rebuild the whole kitchen — as long as the menu is stable you never notice. That is loose coupling.
- Service: a business capability reachable across the network with a clear owner. "Get account balance" is a service;
AccountRepositoryis an internal class, not a service. - Contract: a formal, machine-readable description of the operations, their inputs and outputs, and the errors they can return. In the SOAP world that contract is a WSDL file.
- Loose coupling: the consumer depends only on the contract — not on your language, your database, or your library versions.
Part 1 — What SOA actually means
Picture a large enterprise: core banking, CRM, a card system, loans, reporting — each bought or built in a different decade, in a different language. Now the business says: "when a customer takes a loan, pull their credit score, read their balance from core, and record it in CRM."
The easy, fatal route is for each system to reach into another system's database or build a bespoke point-to-point link. Six systems means up to fifteen links; twenty systems means up to a hundred and ninety. That is integration spaghetti.
Diagram: point-to-point chaos versus a service layer — نمودار: آشفتگی نقطهبهنقطه در برابر لایهی سرویس.
flowchart LR
subgraph Before["Point-to-point"]
A1[Core Banking] --- B1[CRM]
A1 --- C1[Cards]
B1 --- C1
C1 --- D1[Loans]
A1 --- D1
end
subgraph After["Service layer"]
A2[Core Banking] --> S[(Service contracts)]
B2[CRM] --> S
C2[Cards] --> S
D2[Loans] --> S
end
SOA (Service-Oriented Architecture) is the architectural answer: put every capability behind an explicit contract and talk only through that contract. Its four real pillars:
- Explicit boundaries. A service call crosses a boundary: it is expensive, it can fail, it needs serialization. Never pretend a remote call is a local method call — that was exactly the mistake earlier RPC generations made.
- Share schema, not class. Two services exchange a schema, not a JAR. If you must import someone's Java class to talk to them, you are not loosely coupled.
- Autonomy. Each service deploys and scales independently and is allowed to fail without taking the rest down.
- Policy-based compatibility. "To talk to me you must sign and send a timestamp" is expressed as a machine-readable policy (WS-Policy), not as an email to the other team.
Nobody can sell you "SOA" — it is an architectural style. What vendors sell is an ESB, a middleware product. The biggest SOA failures of the 2000s came from exactly that confusion: an organisation bought an expensive ESB, poured all its business logic into it, and ended up with a central, untestable single point of failure.
Governance is the set of rules that decides who may define a service, how namespaces are named, how versions are published and retired, and what each SLA is. In classic SOAP a registry called UDDI was meant to be the phone book of services. It effectively died — nobody discovers services dynamically; in practice teams email a WSDL or drop it in a repo. But the idea of a central catalogue survived, and today it is called an API catalogue / developer portal.
When adding one optional field to an XSD needs three committees and six weeks, teams route around it and go straight to the database. Governance that is slower to follow than to bypass will not be followed. The senior rule: automate governance (schema validation in CI, contract compatibility tests) rather than administering it by hand.
SOA versus microservices
| Axis | Classic SOA | Microservices |
|---|---|---|
| Service size | Large, usually aligned to an enterprise system | Small, aligned to a bounded context |
| Data ownership | Usually a shared enterprise database | Database per service |
| Where the smarts live | Smart pipe (the ESB routes and transforms) | Smart endpoints, dumb pipes |
| Contract | WSDL/XSD | OpenAPI/Protobuf/AsyncAPI |
| Protocol | SOAP over HTTP/JMS | HTTP+JSON, gRPC, brokers |
| Deployment | Several services in one application server | One independent container per service |
| Primary goal | Reuse and integration of existing systems | Delivery speed and team independence |
| Transactions | Attempts at distributed transactions (WS-AT, 2PC) | Eventual consistency, Saga |
Boundaries, Saga and inter-service communication are covered in ms-foundations, ms-communication and ms-data; we do not repeat them here.
Start with what they share: both put capability behind an explicit contract to achieve loose coupling. Then give three structural differences: (1) data ownership — SOA typically has a shared enterprise database, microservices have database-per-service; (2) where the intelligence lives — SOA puts routing and transformation in the ESB, microservices push it into the endpoint and keep the pipe dumb; (3) the unit of deployment and organisational ownership — SOA usually has a central integration team, microservices have independent teams. Close with judgement: "microservices without team autonomy are just distributed SOA with more network latency."
Part 2 — SOAP from zero
The letter inside is one thing, but the envelope carries information too: recipient, sender, "confidential", "urgent", a reference number. The postal clerk never reads the letter — he reads the envelope and decides.
SOAP is exactly that: a standard Envelope containing a Body (the letter itself) and a Header (the processing metadata on the outside). Signatures, security tokens, message IDs, reply addresses, sequence numbers for guaranteed delivery — all go in the Header, never in the Body.
Separating Header from Body is what lets intermediaries process a message without understanding its business content — route it, log it, verify its signature. That out-of-band extensibility is what separates SOAP from a plain RPC. (SOAP originally stood for "Simple Object Access Protocol"; since 1.2 it is officially not an acronym any more — it was neither simple nor only about objects.)
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
xmlns:acc="http://bank.example/accounts/v1">
<soap:Header>
<acc:CorrelationId soap:mustUnderstand="true"
soap:role="http://www.w3.org/2003/05/soap-envelope/role/next">
7f3a9c22-1e44-4a0b-9a11-88b2c4d5e6f7
</acc:CorrelationId>
</soap:Header>
<soap:Body>
<acc:GetBalanceRequest>
<acc:accountNumber>IR820540102680020817909002</acc:accountNumber>
<acc:currency>IRR</acc:currency>
</acc:GetBalanceRequest>
</soap:Body>
</soap:Envelope>
Envelopeis the document root.Headeris optional and holds zero or more header blocks.Bodyis mandatory and appears exactly once.mustUnderstand: if it istrueand the targeted receiver does not recognise the block, it must fault and must not process the message.role(1.2) oractor(1.1) targets a block at a node. The three standard 1.2 URIs share the prefixhttp://www.w3.org/2003/05/soap-envelope/role/:next(every intermediary and the ultimate receiver),none(no node may process it),ultimateReceiver. Omittingrolemeans the ultimate receiver. The 1.1 equivalent ofnextishttp://schemas.xmlsoap.org/soap/actor/next.
The classic cross-organisation mystery: the other side ships a new version and starts sending an unknown header block with mustUnderstand="1". Your SOAP stack raises a MustUnderstand fault before your code ever runs, so your application log shows nothing — the endpoint was never invoked. If you ever see "a fault with no log line", look at the raw envelope first.
Diagram: a SOAP message path through an intermediary node — نمودار: مسیر یک پیام SOAP از فرستنده تا گیرندهی نهایی با یک گره میانی.
sequenceDiagram
participant S as Initial sender
participant I as Intermediary gateway
participant R as Ultimate receiver
S->>I: Envelope + Header(security, correlation)
Note over I: Processes headers for role "next"<br/>Relays them, never touches Body
I->>R: Envelope + remaining headers
Note over R: Processes ultimateReceiver headers<br/>Executes the Body operation
R-->>I: Response envelope
I-->>S: Response envelope
Fault — the SOAP error model
In REST you use HTTP status codes. In SOAP the error lives inside the message: a Fault element inside Body. That is fundamental, because SOAP was designed to be transport-independent — over JMS or SMTP there is no HTTP status code at all.
<!-- SOAP 1.1 -->
<soap:Fault xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>soap:Client</faultcode>
<faultstring>Account number is not a valid IBAN</faultstring>
<faultactor>http://bank.example/accounts</faultactor>
<detail>
<acc:ValidationError xmlns:acc="http://bank.example/accounts/v1">
<acc:field>accountNumber</acc:field>
</acc:ValidationError>
</detail>
</soap:Fault>
<!-- SOAP 1.2 -->
<env:Fault xmlns:env="http://www.w3.org/2003/05/soap-envelope"
xmlns:acc="http://bank.example/accounts/v1">
<env:Code>
<env:Value>env:Sender</env:Value>
<env:Subcode><env:Value>acc:InvalidAccount</env:Value></env:Subcode>
</env:Code>
<env:Reason><env:Text xml:lang="en">Not a valid IBAN</env:Text></env:Reason>
<env:Node>http://bank.example/accounts</env:Node>
<env:Detail>
<acc:ValidationError><acc:field>accountNumber</acc:field></acc:ValidationError>
</env:Detail>
</env:Fault>
| Topic | SOAP 1.1 | SOAP 1.2 |
|---|---|---|
| Envelope namespace | http://schemas.xmlsoap.org/soap/envelope/ |
http://www.w3.org/2003/05/soap-envelope |
| Content-Type | text/xml |
application/soap+xml |
| Operation hint | separate SOAPAction HTTP header |
action parameter inside Content-Type |
| Fault code | flat <faultcode> |
nested <Code><Value> plus <Subcode> |
| Fault text | <faultstring>, no language |
<Reason><Text xml:lang="…">, multilingual |
| Actor / details | <faultactor> / <detail> |
<Node>, <Role> / <Detail> |
| Client / server code | Client / Server |
Sender / Receiver |
| Standard codes | VersionMismatch, MustUnderstand |
plus DataEncodingUnknown |
mustUnderstand |
"1" / "0" |
"true" / "false" |
| Header targeting | actor |
role |
| Standards status | W3C Note | W3C Recommendation |
One practical warning: never dump a Java stack trace into <detail>. It leaks information (class names, file paths, library versions) and it breaks the contract, because nothing in your XSD defines it. detail must be a type declared in your schema (see appsec-owasp).
The overwhelming majority of installed enterprise services are still SOAP 1.1, because WS-I Basic Profile 1.1 — the 2000s interoperability yardstick — was built on 1.1 and older toolchains understand it. Building something new where both sides are modern? Prefer 1.2. Talking to a legacy system? Pick 1.1 and do not argue.
SOAP over HTTP — the raw message
POST /accounts/v1 HTTP/1.1
Host: soa.bank.example
Content-Type: text/xml; charset=utf-8
SOAPAction: "http://bank.example/accounts/v1/GetBalance"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetBalanceRequest xmlns="http://bank.example/accounts/v1">
<accountNumber>IR820540102680020817909002</accountNumber>
</GetBalanceRequest>
</soap:Body>
</soap:Envelope>
# SOAP 1.1 — SOAPAction is its own header and the quotes are part of the value
curl -sS -X POST 'https://soa.bank.example/accounts/v1' \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "http://bank.example/accounts/v1/GetBalance"' \
--data-binary @request.xml
# SOAP 1.2 — the action rides inside Content-Type; there is no SOAPAction header
curl -sS -X POST 'https://soa.bank.example/accounts/v1' \
-H 'Content-Type: application/soap+xml; charset=utf-8; action="http://bank.example/accounts/v1/GetBalance"' \
--data-binary @request.xml
Many older servers will reject the message, or route it to the wrong operation, if SOAPAction is missing or blank. Take the correct value from the soapAction attribute on <soap:operation> in the WSDL — and in 1.1 do not strip the quotes.
Why is SOAP a protocol and REST a style? SOAP has a formal specification that mandates message structure, a processing model, an error model and extension rules; a message is either valid or it is not. REST is a set of constraints (statelessness, uniform interface, resources, cacheability); no standard tells you what your error body looks like — we later filled that gap with application/problem+json (RFC 9457). REST design is covered in api-design.
Say that SOAP has a formal W3C specification mandating message structure (Envelope/Header/Body), a processing model (roles, mustUnderstand), an error model (Fault) and an extension mechanism — and that it is defined independently of transport, so the same message is valid over HTTP, JMS or SMTP. REST is an architectural style with six constraints, not a message format; nothing in REST says what an error body looks like or where authentication belongs. Conclude: SOAP solves interoperability inside the protocol and pays for it in verbosity and complexity; REST delegates it to team agreement and pays for it in inconsistency.
Part 3 — XSD: the language that defines the shape of data
"Name" up to 50 characters, "national ID" exactly 10 digits, "phone" optional, "account type" one of three ticks. Fill it in wrongly and the clerk hands it straight back — it never reaches the accounts department. XSD (XML Schema Definition) is that printed form for XML, and the "clerk" is a validator that rejects malformed input before your code ever sees it.
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:acc="http://bank.example/accounts/v1"
targetNamespace="http://bank.example/accounts/v1"
elementFormDefault="qualified" version="1.0">
<xsd:simpleType name="Iban"> <!-- a base type with a restriction -->
<xsd:restriction base="xsd:string">
<xsd:pattern value="[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="Money"> <!-- a type with children -->
<xsd:sequence>
<xsd:element name="amount" type="xsd:decimal"/>
<xsd:element name="currency" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="GetBalanceRequest"> <!-- a top-level element -->
<xsd:complexType>
<xsd:sequence>
<xsd:element name="accountNumber" type="acc:Iban"/>
<xsd:element name="asOf" type="xsd:date" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="GetBalanceResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="available" type="acc:Money"/>
<xsd:element name="hold" type="acc:Money" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
targetNamespacedeclares which namespace this schema defines elements for. A namespace is just a unique URI-shaped identifier; it need not resolve to anything.elementFormDefault="qualified"means child elements must also carry the namespace.xsd:sequenceenforces order,xsd:choiceallows exactly one branch,xsd:allallows any order (heavily restricted in XSD 1.0).minOccurs="0"optional,maxOccurs="unbounded"repeating,nillable="true"means present-but-explicitly-null viaxsi:nil="true"— different from absent.xsd:includepulls in a schema with the same namespace;xsd:importone with a different namespace. Put shared enterprise types (Money,Address) in their own versioned namespace and import them — but keep that set small, or every tiny change forces every service to rebuild.
Design note: always use xsd:decimal for money (never xsd:float/xsd:double), and give identifiers a pattern instead of a bare xsd:string, so bad data is rejected at the boundary rather than inside your logic.
Its default is unqualified. Your message then sends the root element namespace-qualified but <accountNumber> bare. The other side, which read the schema as qualified, returns a vague "unexpected element" fault. Practical rule: always set elementFormDefault="qualified".
To validate from Java, build a Schema via SchemaFactory and blank the XMLConstants.ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_SCHEMA properties so the parser never touches the network or the filesystem.
Compiling a large XSD costs tens of milliseconds. A Schema object is thread-safe — build it once and hold it in a @Bean. A Validator is not — create one per message. Rebuilding the Schema per request is the classic reason an XML gateway burns CPU.
Part 4 — Reading a WSDL line by line
The datasheet says what it takes in, what it puts out, what standard its sockets follow and where it gets installed. WSDL is that datasheet, except machine-readable: tooling generates code from it. Its five parts answer exactly those questions: what data (types), what message (message), what operations (portType), over what protocol (binding), at what address (service).
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:acc="http://bank.example/accounts/v1"
xmlns:tns="http://bank.example/accounts/v1/wsdl"
targetNamespace="http://bank.example/accounts/v1/wsdl" name="AccountService">
<wsdl:types> <!-- 1) data types -->
<xsd:schema targetNamespace="http://bank.example/accounts/v1/wsdl">
<xsd:import namespace="http://bank.example/accounts/v1"
schemaLocation="accounts-v1.xsd"/>
</xsd:schema>
</wsdl:types>
<wsdl:message name="GetBalanceRequest"> <!-- 2) abstract messages -->
<wsdl:part name="parameters" element="acc:GetBalanceRequest"/>
</wsdl:message>
<wsdl:message name="GetBalanceResponse">
<wsdl:part name="parameters" element="acc:GetBalanceResponse"/>
</wsdl:message>
<wsdl:portType name="AccountPortType"> <!-- 3) abstract interface -->
<wsdl:operation name="GetBalance">
<wsdl:input message="tns:GetBalanceRequest"/>
<wsdl:output message="tns:GetBalanceResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="AccountSoapBinding" type="tns:AccountPortType"> <!-- 4) protocol -->
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="GetBalance">
<soap:operation soapAction="http://bank.example/accounts/v1/GetBalance"/>
<wsdl:input><soap:body use="literal"/></wsdl:input>
<wsdl:output><soap:body use="literal"/></wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="AccountService"> <!-- 5) the real address -->
<wsdl:port name="AccountSoapPort" binding="tns:AccountSoapBinding">
<soap:address location="https://soa.bank.example/accounts/v1"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
Diagram: WSDL structure, from abstract contract down to a concrete address — نمودار: ساختار WSDL از انتزاع تا آدرس واقعی.
flowchart TD
T[types: XSD element definitions] --> M[message: named parts]
M --> P["portType / interface: operations<br/>input, output, fault"]
P --> B["binding: protocol + style + use<br/>soapAction per operation"]
B --> S["service and port<br/>soap:address location"]
The top three layers are abstract, the bottom two concrete. The same portType can have two bindings (one SOAP over HTTP, one over JMS), and the same binding can be published at several port addresses. That is the separation of contract from transport.
Two binding attributes decide the shape of your message: style (document or rpc) and use (literal or encoded).
| Combination | What the Body looks like | XSD-validatable | Status today |
|---|---|---|---|
rpc/encoded |
operation name plus xsi:type-tagged, multi-ref values |
no | forbidden by WS-I Basic Profile 1.1 |
rpc/literal |
operation name as wrapper, children unqualified | partly | rare |
document/literal bare |
one XSD-defined element, no wrapper | yes | operations with a single parameter |
document/literal wrapped |
element named after the operation wrapping the parameters | yes | today's industry default |
wrapped is a naming convention, not a WSDL value: you get the RPC benefit of reading the operation from the Body root and full XSD validation.
The message cannot be validated against an XSD, multi-reference graphs (href/id) appear, and every stack serialises them slightly differently. If the WSDL you are handed says use="encoded", you are wiring into a pre-2005 system: budget extra time for serialisation debugging and archive the raw payloads.
WSDL 1.1 vs 2.0: the root moved from definitions to description, portType became interface (with extends), port became endpoint, and message disappeared entirely (you reference the XSD element directly); the namespace became http://www.w3.org/ns/wsdl. WSDL 2.0 became a W3C Recommendation in 2007 but the market never adopted it — Java and .NET tooling has patchy support, and 99% of what you will meet is WSDL 1.1.
When the WSDL is generated dynamically, soap:address location is built from the incoming HTTP request. Behind an ingress that often becomes http://10.0.3.14:8080/... — the pod's internal IP. The client reads it, dials it, and times out. Fix: setTransformWsdlLocations(true) in Spring-WS, publishedEndpointUrl in CXF, and make the proxy send (and the app honour) X-Forwarded-Proto/X-Forwarded-Host.
Name all five: types defines the data with XSD; message builds abstract messages from parts; portType (WSDL 2.0: interface) defines operations with input/output/fault — those three are abstract. binding says which protocol and which style/use carry the operation, and service/port gives the network address — those two are concrete. Add why the split matters: one abstract contract can be published over HTTP and JMS simultaneously without changing the contract.
style decides how the Body is composed; use decides how values are serialised. In rpc/encoded the operation name is the Body root and values carry xsi:type and href references — so the message cannot be validated against an XSD and stacks disagree on the encoding; WS-I Basic Profile 1.1 banned it. In document/literal wrapped the Body is a schema-defined element named after the operation: you get full validation and you can route on the Body root. Today wrapped is the practical default.
Part 5 — Contract-first versus code-first
Diagram: contract-first versus code-first generation paths — نمودار: دو مسیر تولید کد.
flowchart LR
subgraph CF["Contract-first"]
X[XSD + WSDL] -->|wsdl2java / wsimport| J1[Generated Java stubs]
J1 --> I1[Your implementation]
end
subgraph CoF["Code-first"]
J2["@WebService Java class"] -->|wsgen / runtime| W2["Generated WSDL"]
W2 --> C2[Consumers]
end
Not because it is "cleaner". Because in code-first your contract is a function of your implementation details: renaming a field, upgrading JAXB, even reordering methods can change the WSDL and break the other side's generated clients — with nothing turning red in your repo. In contract-first the WSDL is a versioned artefact in VCS and every change shows up in code review. The exception: a purely internal, short-lived service where both ends belong to one team.
The code-first route with JAX-WS: annotate the class with @WebService(name=…, serviceName=…, targetNamespace=…), pin the message style with @SOAPBinding(style=DOCUMENT, use=LITERAL, parameterStyle=WRAPPED), and name every operation, parameter and result with @WebMethod(operationName=…, action=…), @WebParam(name=…, targetNamespace=…) and @WebResult. If you leave those names implicit, the framework derives them from your Java method and parameter names — meaning your contract is tied to local variable names. Since Jakarta EE 9 the packages moved from javax.jws to jakarta.jws (see java-ee-jakarta).
JAX-WS left Java SE 11 with JEP 320, so those commands do not exist on a modern JDK. The right move today is to pull the tooling in as a Maven/Gradle dependency — org.apache.cxf:cxf-codegen-plugin (goal wsdl2java, version 4.2.3) or com.sun.xml.ws:jaxws-maven-plugin (goal wsimport, version 4.0.5, the Eclipse Metro reference implementation).
Bind the CXF plugin to the generate-sources phase, set sourceRoot to target/generated-sources/cxf, and point wsdlOptions/wsdlOption/wsdl at the WSDL file; extra switches (-p to map a namespace to a package, -validate) go inside extraargs. The same operations from the command line:
wsdl2java -d target/generated-sources -p example.accounts.gen \
-validate src/main/resources/wsdl/accounts-v1.wsdl # client + data types
wsdl2java -impl -server src/main/resources/wsdl/accounts-v1.wsdl # server skeleton
xjc -d target/generated-sources -p example.accounts.gen accounts-v1.xsd # XSD only
It is tempting to commit the wsdl2java output "to speed up the build". Don't: six months later the WSDL and the generated code have drifted and nobody knows which is authoritative. Commit the WSDL/XSD and generate in generate-sources.
Contract-first — and build the argument on contract ownership: in code-first the WSDL is a by-product of the code, so a harmless refactor can change the contract and break clients with no CI signal at all. In contract-first the XSD/WSDL is a versioned artefact you can put compatibility tests around. Then name the exception: for a short-lived internal service owned end-to-end by one team, code-first is faster. That shows you understood the rule rather than memorised it.
Part 6 — XML in Java: JAXB, performance and safety
JAXB (Jakarta XML Binding) bridges XML and Java objects: marshalling is object → XML, unmarshalling is XML → object. The mapping you should know by heart:
| XSD construct | Java result |
|---|---|
top-level xsd:element |
class with @XmlRootElement, or a JAXBElement<T> in ObjectFactory |
xsd:complexType |
class with @XmlType |
minOccurs="0" |
nullable field (wrapper type, not a primitive) |
maxOccurs="unbounded" |
List<T> |
xsd:enumeration |
Java enum with @XmlEnumValue |
xsd:dateTime |
XMLGregorianCalendar by default |
xsd:decimal |
BigDecimal |
double cannot hold 0.1 exactly, and after a few additions a balance drifts — in a bank that means an overnight reconciliation break. xsd:decimal must become BigDecimal; if your generator does otherwise, fix it with <jaxb:javaType> in a binding file. That same binding file is where an XmlAdapter lets you map xsd:date to java.time.LocalDate instead of the painful XMLGregorianCalendar.
Building a JAXBContext reflects over the whole package and takes tens to hundreds of milliseconds. Put it inside a service method and it burns CPU under load. Build it once (static or a @Bean). Conversely Marshaller/Unmarshaller are not thread-safe — never make them static.
XML security: XXE and entity bombs
XXE (XML External Entity) is when an attacker declares an external entity in the input and your parser cheerfully reads a server file or calls an internal address:
<!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<GetBalanceRequest xmlns="http://bank.example/accounts/v1">
<accountNumber>&xxe;</accountNumber>
</GetBalanceRequest>
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
XMLReader reader = spf.newSAXParser().getXMLReader();
Object result = CTX.createUnmarshaller()
.unmarshal(new SAXSource(reader, new InputSource(inputStream)));
// If you use StAX instead:
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
That overload builds a parser with default settings, and defaults are not always safe. Always construct a hardened SAXSource or StAXSource yourself — and centralise it in one @Bean rather than repeating it across ten endpoints. Do not forget the second attack, billion laughs: a ten-kilobyte document that expands into gigabytes through nested entities. Banning DOCTYPE closes both (see appsec-owasp).
Part 7 — Spring Web Services in practice
Spring-WS is deliberately contract-first only, and it works like Spring MVC with one difference: dispatching is by payload root, not by URL. Version 4.1.x sits on Spring Framework 6 and Jakarta EE 10; version 5.0.0 (November 2025) moved to Spring Framework 7, Spring Security 7 and Jakarta EE 11 (Servlet 6.1) with full JSpecify null-safety. Dependencies: spring-boot-starter-web-services plus wsdl4j.
The entry point is a MessageDispatcherServlet registered on /ws/*; always call setTransformWsdlLocations(true) on it so the address inside the published WSDL is rewritten with the real request host.
@EnableWs
@Configuration
public class WebServiceConfig implements WsConfigurer {
@Bean(name = "accounts") // the bean name becomes the path: /ws/accounts.wsdl
public DefaultWsdl11Definition accountsWsdl() {
DefaultWsdl11Definition wsdl = new DefaultWsdl11Definition();
wsdl.setPortTypeName("AccountPortType");
wsdl.setLocationUri("/ws");
wsdl.setTargetNamespace("http://bank.example/accounts/v1/wsdl");
wsdl.setSchema(new SimpleXsdSchema(new ClassPathResource("xsd/accounts-v1.xsd")));
return wsdl;
}
@Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
PayloadValidatingInterceptor v = new PayloadValidatingInterceptor();
v.setSchema(new ClassPathResource("xsd/accounts-v1.xsd"));
v.setValidateRequest(true);
v.setValidateResponse(true);
interceptors.add(v);
}
}
@Endpoint
public class AccountEndpoint {
@PayloadRoot(namespace = "http://bank.example/accounts/v1",
localPart = "GetBalanceRequest")
@ResponsePayload
public GetBalanceResponse getBalance(@RequestPayload GetBalanceRequest request) {
return balances.lookup(request.getAccountNumber());
}
}
DefaultWsdl11Definition generates the WSDL from the XSD using a naming convention: an element suffixed Request is an input, its Response twin the output, and Fault the error. Dispatch is on the namespace + localPart of the Body root element — so ten operations can share one URL. To turn an exception into a fault, annotate the exception class with @SoapFault(faultCode = FaultCode.CLIENT, faultStringOrReason = "…"). On the client side, build a WebServiceTemplate with a Jaxb2Marshaller and an HttpComponents5MessageSender (which takes connectionTimeout and readTimeout) and call marshalSendAndReceive.
Most teams only validate requests. But if your code ever emits an invalid response — a null in a mandatory field, say — the client swallows it and the error surfaces in their system, and their support desk calls you. validateResponse converts that into a loggable failure on your side for a few milliseconds of cost.
Part 8 — Apache CXF
CXF covers both JAX-WS (SOAP) and JAX-RS (REST) and has far deeper WS-* support. The stable line is 4.2.x (4.2.3 in mid-2026), targeting Jakarta EE 11 and JDK 17+; use org.apache.cxf:cxf-spring-boot-starter-jaxws with cxf.path=/services. On the server you create an EndpointImpl and publish it with setAddress("/accounts/v1"), setWsdlLocation("classpath:wsdl/accounts-v1.wsdl") and setPublishedEndpointUrl(...) — that last one is CXF's fix for the internal-pod-address-in-WSDL trap. On the client:
JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
factory.setServiceClass(AccountPortType.class);
factory.setAddress("https://soa.bank.example/services/accounts/v1");
AccountPortType port = (AccountPortType) factory.create();
HTTPConduit conduit = (HTTPConduit) ClientProxy.getClient(port).getConduit();
HTTPClientPolicy policy = new HTTPClientPolicy();
policy.setConnectionTimeout(3_000); // milliseconds
policy.setReceiveTimeout(10_000);
policy.setAllowChunking(false); // some legacy servers cannot handle chunking
conduit.setClient(policy);
Some SOAP stacks default to waiting forever. If the upstream hangs, your server threads block on read() one by one until the pool is exhausted and the entire application dies — including the parts that have nothing to do with that service. This is the textbook cascading failure. Always set connectionTimeout and receiveTimeout, and wrap the call in a circuit breaker and a bulkhead (see resilience).
| Criterion | Spring-WS | Apache CXF | JAX-WS RI (Metro) |
|---|---|---|---|
| Philosophy | contract-first only | both, contract-first recommended | both, code-first easiest |
| Dispatching | by payload root | by JAX-WS operation | by JAX-WS operation |
| WS-* coverage | Security via WSS4J | full: Security, Addressing, RM, Policy | good, via Metro add-ons |
| Best for | a new service with your own contract | enterprise integration, heavy WS-* | a quick, simple service |
Say CXF, and justify it: it has full WS-SecurityPolicy support, meaning you can read the security policy out of the WSDL itself and let CXF apply the signing and encryption, instead of hand-wiring interceptors; it also brings WS-Addressing and WS-RM, which enterprise integrations occasionally require. Then show you are not dogmatic: if the project were only a fresh service with our own contract, Spring-WS is simpler and more strictly contract-first. Close by noting that in either case timeouts, message-size limits and XML hardening are still yours to configure.
Part 9 — The WS-* specs a bank will ask about
"WS-*" is a family of specifications layered on SOAP, each solving one cross-cutting concern. They all live in the message header, which is why they work independently of transport.
9.1 WS-Security — security at the message level
TLS gives you transport-level security: encrypted from A to B, but plaintext the moment it lands on B. If the message crosses three gateways it is exposed at all three. WS-Security gives you message-level security: the signature and encryption sit on the content itself and survive to the final destination. Cryptographic fundamentals are in crypto-foundations and pki-tls-gpg.
<soap:Header>
<wsse:Security soap:mustUnderstand="1"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="TS-1">
<wsu:Created>2026-08-13T09:15:00Z</wsu:Created>
<wsu:Expires>2026-08-13T09:20:00Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id="UT-1">
<wsse:Username>loan-service</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">s3cr3t</wsse:Password>
<wsse:Nonce>5uW2rF0z2Q0yQ0uH2QW5wA==</wsse:Nonce>
<wsu:Created>2026-08-13T09:15:00Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
Switch the Type to …#PasswordDigest and the password value becomes Base64( SHA-1( nonce + created + password ) ).
Two traps: (1) to recompute the digest the server must store the password in cleartext or reversibly — exactly what modern practice forbids, so you cannot hash it with bcrypt/Argon2; (2) the algorithm is SHA-1. In practice the digest only prevents sniffing the password on the wire, which TLS does better. Today's recommendation: PasswordText over mandatory TLS, plus a short-window Timestamp and a nonce cache — or better, client-certificate authentication (mTLS) and X.509 signatures.
The real power of WS-Security is signing and encrypting parts of the message: "encrypt only <CardNumber> and sign the whole Body". Configuring that in CXF with WSS4J (current line 4.0.x):
Map<String, Object> p = new HashMap<>();
p.put(ConfigurationConstants.ACTION, ConfigurationConstants.TIMESTAMP + " "
+ ConfigurationConstants.USERNAME_TOKEN + " " + ConfigurationConstants.SIGNATURE);
p.put(ConfigurationConstants.USER, "loan-service");
p.put(ConfigurationConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
p.put(ConfigurationConstants.PW_CALLBACK_REF, new ClientPasswordCallback());
p.put(ConfigurationConstants.SIGNATURE_USER, "loan-signing-key");
p.put(ConfigurationConstants.SIG_ALGO, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
p.put(ConfigurationConstants.SIG_PROP_FILE, "client-crypto.properties");
p.put(ConfigurationConstants.SIGNATURE_PARTS,
"{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body");
p.put(ConfigurationConstants.TTL_TIMESTAMP, "300");
client.getOutInterceptors().add(new WSS4JOutInterceptor(p));
client-crypto.properties points at the keystore via org.apache.wss4j.crypto.merlin.keystore.type=pkcs12, …keystore.file and …keystore.alias. In Spring-WS the same job is done by Wss4jSecurityInterceptor with validationActions="Timestamp UsernameToken Signature", timestampStrict=true and timeToLive=300.
keystore.password=changeit in the repo is a leak from day one; passwords must come from a secret manager or an environment variable, and the keystore must not be baked into the container image (see key-management-secrets). And certificates expire: put a 30-day expiry alert in place, or one morning every transaction fails with "signature verification failed".
Diagram: receiver-side WS-Security processing order — نمودار: ترتیب پردازش امنیت پیام در سمت گیرنده.
flowchart TD
A[Raw envelope arrives] --> B{Timestamp valid and fresh?}
B -- no --> F[Fault: InvalidSecurityToken]
B -- yes --> C{Nonce already seen?}
C -- yes --> F
C -- no --> D[Verify signature over signed parts]
D -- invalid --> F
D -- valid --> E[Decrypt encrypted parts]
E --> G[Authenticate UsernameToken or X.509 subject]
G --> H[Dispatch to endpoint]
9.2 WS-Addressing
Over HTTP the "destination" is the URL and the reply comes back on the same connection. But what if the message travels over JMS? Or the reply must go somewhere else entirely? WS-Addressing moves that information out of the transport and into the message header. The namespace (W3C Recommendation, 2006) is http://www.w3.org/2005/08/addressing.
<soap:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:To>https://soa.bank.example/services/accounts/v1</wsa:To>
<wsa:Action>http://bank.example/accounts/v1/GetBalance</wsa:Action>
<wsa:MessageID>urn:uuid:7f3a9c22-1e44-4a0b-9a11-88b2c4d5e6f7</wsa:MessageID>
<wsa:ReplyTo><wsa:Address>https://loans.bank.example/callbacks</wsa:Address></wsa:ReplyTo>
</soap:Header>
The response carries <wsa:RelatesTo> echoing the request's MessageID — standard, machine-readable correlation. Enable it in CXF with @Addressing(enabled = true, required = true) on the implementation class.
Diagram: asynchronous callback correlation with WS-Addressing — نمودار: پاسخ نامتقارن با WS-Addressing.
sequenceDiagram
participant C as Consumer
participant P as Provider
participant CB as Callback endpoint
C->>P: Request (MessageID=urn:uuid:A, ReplyTo=CB)
P-->>C: HTTP 202 Accepted (no body)
Note over P: Long-running processing
P->>CB: Response (RelatesTo=urn:uuid:A)
CB-->>P: HTTP 202 Accepted
Treat MessageID as your idempotency key and trace identifier. If a client retries after a timeout with the same MessageID, the server can detect it and replay the cached response instead of moving the money twice. It is the same pattern we build in REST with an Idempotency-Key header — only here it is standardised.
9.3 WS-ReliableMessaging
Sender and receiver establish a sequence; every message carries a MessageNumber and the receiver returns a SequenceAcknowledgement. No ack, resend. The selectable guarantees are AtMostOnce, AtLeastOnce, ExactlyOnce and InOrder.
Diagram: lifecycle of a WS-RM sequence — نمودار: چرخهی حیات یک sequence.
stateDiagram-v2
[*] --> Creating: CreateSequence
Creating --> Active: CreateSequenceResponse
Active --> Active: Message(n) / Acknowledgement
Active --> Retrying: ack timeout
Retrying --> Active: ack received
Retrying --> Faulted: max retries exceeded
Active --> Terminated: TerminateSequence
Faulted --> [*]
Terminated --> [*]
WS-RM builds reliability in the memory of the two endpoints; if the sender restarts without a configured persistent store, the sequence is lost. And "exactly once" applies to message delivery, not to business effect: if the receiver processes the message and dies before acknowledging, it will be redelivered. In 2026, if you genuinely need reliable delivery the right answer is usually a broker (messaging, rabbitmq-amqp) plus the outbox pattern and an idempotent consumer. Pick WS-RM when the counterparty mandates it.
9.4 MTOM — attachments without base64 bloat
A one-megabyte PDF inside XML must be base64-encoded: roughly 33% larger, and the whole string sits in memory. MTOM, using XOP, pulls the bytes out into a separate MIME part and leaves only a pointer in the XML; the whole message becomes a multipart/related; type="application/xop+xml":
<content>
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
href="cid:statement@example"/>
</content>
The matching MIME part carries Content-ID: <statement@example> and the raw PDF bytes. In Java you simply declare the element as xsd:base64Binary with xmime:expectedContentTypes (it maps to a DataHandler) and put @MTOM(enabled = true, threshold = 4096) on the implementation.
Below roughly 100 KB: base64 inline, no MIME complexity. From 100 KB to a few MB: MTOM. Above a few MB: do not push the file through SOAP at all — store it in object storage and send only a short-lived signed URL. That saves server memory, makes retries cheap, and enables resumable uploads.
Frame it as levels: TLS is transport-level and only valid between two connection endpoints; wherever TLS terminates — a gateway, a load balancer — the message is plaintext. WS-Security is message-level and survives to the ultimate receiver, even across JMS. Name three things TLS cannot do: (1) sign part of a message and encrypt another part — encrypt only the card number while leaving routing fields readable; (2) non-repudiation — the digital signature is storable, so you can later prove who sent what; (3) transport independence. Finish by saying that in practice you use both, not one instead of the other.
Three layers: (1) wsu:Timestamp with a short window (say five minutes) and NTP-synchronised clocks; (2) a nonce cache — keep every wsse:Nonce or wsa:MessageID for the length of the window in a shared store (Redis with a TTL) and reject duplicates; in a multi-instance deployment that cache must be shared, otherwise the attacker simply hits another instance; (3) a digital signature over the Timestamp and Body so the attacker cannot rewrite the time. For extra credit add that financial operations also need business-level idempotency: a unique transaction key with a unique constraint in the database.
Part 10 — SOAP versus REST
| Axis | SOAP | REST |
|---|---|---|
| Nature | protocol with a formal specification | architectural style, a set of constraints |
| Contract | WSDL + XSD, mandatory and machine-readable | OpenAPI, optional and descriptive |
| Message format | XML only | JSON (usually), XML, anything |
| Transport | HTTP, JMS, SMTP | in practice HTTP only |
| Error model | standard Fault inside the message |
HTTP status code + free-form body (RFC 9457) |
| Security | WS-Security at message level + TLS | TLS + OAuth2/JWT at transport level |
| Transactions | WS-AT (two-phase, rarely used) | none; Saga at application level |
| Versioning | new namespace in the XSD | /v2 path, header, media type |
| Caching | effectively none (everything is POST) | native to HTTP (ETag, Cache-Control) |
| Size and tooling | heavy messages, very mature codegen | light messages, codegen from OpenAPI |
| Best for | cross-organisation, banking, government | public APIs, mobile, internal microservices |
When does a bank still pick SOAP? (1) A partner or regulator mandates it — they hand you a WSDL and it is not negotiable; (2) an enforceable contract — an XSD that rejects a malformed message at the boundary has real operational value; (3) digital signatures and non-repudiation — when you must prove years later that a message came from a specific party unaltered; (4) existing investment — hundreds of deployed services, gateways, monitoring and runbooks.
In practice it is almost never "SOAP or REST", it is "SOAP and REST": you either build a REST facade over SOAP (a mobile app should not have to speak XML) or a SOAP facade over REST (your new services are REST but a legacy partner only speaks SOAP). In both cases the adapter must translate only — business logic must not accumulate inside it.
For a new API in 2026, REST or gRPC (grpc-protobuf) is nearly always right. But answering "SOAP is bad, migrate everything" scores badly in a senior interview, because it shows you do not see migration cost, commercial risk or contractual obligations. The mature answer: build the new edge modern, put the old services behind a facade, and when you do replace one, keep the old contract — the new service publishes the same WSDL, traffic shifts gradually, and responses are shadow-compared. The common mistake is changing implementation and contract at the same time and then not knowing which change caused the failure.
Part 11 — ESB and integration patterns
Letters arrive in every shape. The sorting office reads the envelope, decides which city it goes to, and if necessary repackages it; undeliverable mail goes to the dead-letter shelf. An ESB (Enterprise Service Bus) is that for software messages: a middle layer doing routing, format transformation, protocol adaptation and error handling so systems never have to know about each other.
| Pattern (EIP) | One line | Banking example |
|---|---|---|
| Content-Based Router | choose the destination from the content | internal to core, interbank to a gateway |
| Message Translator | convert format A into format B | ISO 8583 into ISO 20022 XML |
| Splitter / Aggregator | break a composite up / collect responses | payroll batch file → one transfer per row |
| Normalizer | map many formats to one canonical shape | three branch formats → one canonical schema |
| Wire Tap | take a copy for monitoring | a copy of the message to the audit system |
| Dead Letter Channel | quarantine unprocessable messages | the payments.dlq queue |
| Idempotent Consumer | never apply a duplicate twice | unique transaction key |
| Claim Check | keep bulk data out of the message | PDF in object storage, URL in the message |
Canonical data model: instead of N×(N−1) translations, define one shared language so each system only translates to and from canonical — N² becomes 2N.
The classic trap is building one canonical Customer that satisfies every system's needs. The result is a complexType with 200 fields, 190 of them always empty, where every small change goes through a committee. The right move: keep the canonical model small and scoped to one domain (a canonical for "payment", not one for the whole bank) — the bounded-context discussion in ddd.
Apache Camel implements those patterns as code; the current LTS line is 4.22.x (August 2026).
public class PaymentRoute extends RouteBuilder {
@Override
public void configure() {
errorHandler(deadLetterChannel("jms:queue:payments.dlq")
.maximumRedeliveries(3).redeliveryDelay(2000).backOffMultiplier(2));
from("cxf:bean:paymentSoapEndpoint") // SOAP intake
.idempotentConsumer(simple("${body.transactionId}"),
MemoryIdempotentRepository.memoryIdempotentRepository(10_000))
.wireTap("jms:queue:audit.copy") // Wire Tap
.bean(PaymentNormaliser.class, "toCanonical") // Message Translator
.choice() // Content-Based Router
.when(simple("${body.channel} == 'INTERNAL'")).to("jms:queue:core.transfer")
.when(simple("${body.amount} > 500000000")).to("jms:queue:aml.review")
.otherwise().to("cxf:bean:interbankSoapClient")
.end();
}
}
Diagram: a realistic integration route composed of several EIPs — نمودار: یک مسیر یکپارچهسازی واقعی.
flowchart LR
A[SOAP endpoint] --> B[Idempotent consumer]
B --> C[Wire tap to audit]
B --> D[Translator: legacy to canonical]
D --> E{Content-based router}
E -->|internal| F[Core transfer queue]
E -->|high value| G[AML review queue]
E -->|other| H[Interbank SOAP client]
H -->|error after retries| I[(Dead letter queue)]
Traditional ESBs had three structural problems: a central bottleneck and single point of failure (one bad route takes the whole bus down); unclear ownership (business logic lived in the ESB's GUI console, with no code review and no automated tests); and an organisational bottleneck (every small change queued behind the integration team). The industry's answer was "smart endpoints, dumb pipes". That did not kill the patterns — Camel is alive and widely used, it is just a library inside your service instead of a central product.
Do not rip it out overnight: (1) move routes out of the GUI console into versioned code; (2) hand each domain's routes to that domain's owner; (3) every time a route changes, move it from the bus into the service at that moment (strangler). It takes three years, but the risk is controllable — unlike a big rewrite.
Part 12 — Testing, debugging and operations
SoapUI (open-source line 5.x) is still the fastest way into an unfamiliar WSDL: New SOAP Project with the WSDL URL builds a sample request per operation; in Auth you configure WSS-Username Token or WSS Signature; the Raw tab shows the exact bytes on the wire and solves 90% of debugging sessions; and MockService generates a stand-in service from the same WSDL. Run that mock in CI — the day the counterparty changes their WSDL, the mock stops matching your code and you find out before production. Lighter Java alternatives: WireMock or Testcontainers (see testing). In Postman you build it by hand: POST, a raw → XML body, and the Content-Type and SOAPAction headers.
curl -v --http1.1 -X POST 'https://soa.bank.example/services/accounts/v1' \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "http://bank.example/accounts/v1/GetBalance"' \
--cert client.pem --key client.key --cacert ca.pem \
--data-binary @request.xml | xmllint --format -
xmllint --noout --schema accounts-v1.xsd response-body.xml # validate a saved response
The common and dangerous pattern: someone turns on LoggingInInterceptor to debug and forgets to turn it off. Now card numbers, national IDs and even wsse:Password sit in the log file and then in the central log platform — where dozens of people have access and everything is backed up. In production log metadata only; if you truly need bodies, use CXF's LoggingFeature with setLimit(8192), setLogBinary(false) and setSensitiveElementNames(...) (see logging).
A typical audit table — a trace of messages that stores no sensitive content:
CREATE TABLE soap_audit (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
correlation text NOT NULL,
operation text NOT NULL,
fault_code text,
duration_ms integer,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_fault ON soap_audit (created_at DESC) WHERE fault_code IS NOT NULL;CREATE TABLE soap_audit (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
correlation VARCHAR2(64) NOT NULL,
operation VARCHAR2(128) NOT NULL,
fault_code VARCHAR2(128),
duration_ms NUMBER(10),
created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL
);
-- Oracle has no partial index; this function-based index stores no fault-free rows
CREATE INDEX idx_audit_fault
ON soap_audit (CASE WHEN fault_code IS NULL THEN NULL ELSE created_at END);PostgreSQL has partial indexes (WHERE), so you can say "index only the rows that carry a fault". Oracle has no such thing, but an Oracle B-tree index does not store rows whose key columns are all NULL — so a function-based index with CASE achieves the same effect (see oracle-postgres-dialects).
| Symptom | Likely meaning | First thing to do |
|---|---|---|
MustUnderstand fault |
you do not recognise a mustUnderstand="1" header |
look at the raw envelope |
VersionMismatch |
wrong envelope namespace | check the binding and Content-Type |
Client/Sender with no detail |
the message fails schema validation | validate locally with xmllint --schema |
| HTTP 415 | wrong Content-Type |
text/xml versus application/soap+xml |
Unexpected element |
child-namespace mismatch | check elementFormDefault |
Invalid timestamp |
clock skew | NTP on both servers |
Signature verification failed |
c14n, certificate, or an intermediary rewrote it | make sure nothing reformats the XML |
An XML signature is computed over the canonicalized form of the document. If a gateway or logger pretty-prints the message, adds whitespace, or reorders namespace declarations, the signature breaks and the other side reports "signature verification failed" — even though nobody touched the content. A signed message must travel byte-for-byte intact.
For monitoring (see observability) export: request rate per operation, p95 and p99 latency, fault rate broken down by code (a spike in Sender means a client broke, a spike in Receiver means you did), and days remaining until your signing certificate expires. Set hard limits too: in CXF the org.apache.cxf.stax.maxChildElements, maxElementDepth, maxTextLength and attachment-max-size properties stop denial-of-service by oversized payloads.
Part 13 — Financial message formats: ISO 8583 and ISO 20022
ISO 8583 is the card-transaction message standard (ATM, POS, bank switches). Unlike SOAP it is not textual and tag-based; it is binary and positional. It has three parts: an MTI (four digits encoding version, message class, function and origin — 0100 an authorisation request and 0110 its response, 0200 a financial request, 0400 a reversal); a bitmap (a 64-bit map saying which fields are present; bit 1 set means a second bitmap follows); and the data elements.
| Number | Name | Note |
|---|---|---|
| DE 2 | Primary Account Number | the card number (PAN) |
| DE 4 | Transaction Amount | amount, with no decimal point |
| DE 11 | System Trace Audit Number | STAN — the trace number |
| DE 37 | Retrieval Reference Number | RRN — the correlation key |
| DE 39 | Response Code | 00 means approved |
| DE 49 | Currency Code | determines the number of decimal digits |
| DE 55 | ICC Data | EMV chip data |
DE 4 is a fixed-length integer and the number of decimal places comes from the currency code (DE 49). So 000000012345 may be 123.45 dollars or 12345 rials. Get that wrong in your translator and amounts come out a hundred times off — and your tests will not catch it, because you wrote them with a single currency.
ISO 20022 is an XML-based financial messaging standard that replaces positional bitmaps with named, structured fields. Message names follow a pattern: pacs.008 (customer credit transfer), pacs.002 (status report), pain.001 (payment initiation), camt.053 (statement).
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.10">
<FIToFICstmrCdtTrf>
<GrpHdr><MsgId>MSG-20260813-0001</MsgId><NbOfTxs>1</NbOfTxs></GrpHdr>
<CdtTrfTxInf>
<PmtId><EndToEndId>E2E-778812</EndToEndId></PmtId>
<IntrBkSttlmAmt Ccy="EUR">1250.00</IntrBkSttlmAmt>
<Dbtr><Nm>Sender Name</Nm></Dbtr>
</CdtTrfTxInf>
</FIToFICstmrCdtTrf>
</Document>
ISO 20022 is a schema, not a transport: that XML usually travels inside a SOAP message, over a queue, or in a batch file. Which means the skills in this chapter — XSD, namespaces, validation, JAXB, XML signatures — are exactly the skills an ISO 20022 project needs.
"ISO 8583 was built for the card world and real-time authorisation: compact, positional, optimised for narrow bandwidth. ISO 20022 was built for a data-rich world: structured XML with named fields carrying full party and remittance information, which is essential for compliance and anti-money-laundering. Institutions in transition run both rails in parallel, and the biggest engineering challenge is lossless translation between them — because 8583 simply does not carry the fields 20022 wants."
Part 14 — Cheat sheet
| What you need | Value / command |
|---|---|
| Envelope namespace 1.1 / 1.2 | http://schemas.xmlsoap.org/soap/envelope/ · http://www.w3.org/2003/05/soap-envelope |
| Content-Type 1.1 / 1.2 | text/xml + SOAPAction header · application/soap+xml; action="…" |
| WSDL 1.1 / SOAP binding namespace | http://schemas.xmlsoap.org/wsdl/ · …/wsdl/soap/ |
| XSD / WS-Addressing namespace | http://www.w3.org/2001/XMLSchema · http://www.w3.org/2005/08/addressing |
wsse / XOP namespace |
…oasis-200401-wss-wssecurity-secext-1.0.xsd · http://www.w3.org/2004/08/xop/include |
| Standard WSDL transport | http://schemas.xmlsoap.org/soap/http |
| Generate from WSDL (CXF) | wsdl2java -d out -p com.example.gen -validate service.wsdl |
| Server skeleton / from XSD only | wsdl2java -impl -server s.wsdl · xjc -d out -p com.example.gen s.xsd |
| Pretty-print / validate XML | xmllint --format f.xml · xmllint --noout --schema s.xsd d.xml |
| Maven plugins: CXF / Metro | cxf-codegen-plugin:wsdl2java · jaxws-maven-plugin:wsimport |
| Spring starter for SOAP | spring-boot-starter-web-services + wsdl4j |
Part 15 — The remaining interview questions
The version lives in the namespace, not the URL: http://bank.example/accounts/v1 and .../v2. Because dispatching keys on the Body root element, two namespaces mean two entirely separate contracts that can coexist on one endpoint. Then the compatibility rule: adding an optional element at the end of an xsd:sequence is backward compatible; changing a type, making a field mandatory, renaming, or reordering a sequence is breaking. One subtle point that scores well: adding a value to an xsd:enumeration looks harmless from the sender's side but an older receiver rejects the message during validation — for fields that grow, use a patterned string rather than an enumeration.
Show a method. First separate network, XML parsing and business logic by timing three points in the pipeline. Then go to the SOAP-specific suspects: (1) a JAXBContext being built per request; (2) validation recompiling the schema every time; (3) large base64 payloads that should be MTOM or claim-check; (4) DOM instead of StAX for big messages; (5) no HTTP connection pool, or keep-alive disabled. If you sign messages, signature verification itself costs CPU — prove it with a profiler. And stress the order: measure first, change second.
Because "what to do" is encoded in the message body, not in the HTTP method; HTTP is only a tunnel. The cost is everything HTTP gives you for free: caching (a POST is not cacheable, so no CDN or proxy can cache a read-only response), idempotency semantics (a proxy cannot know whether a retry is safe), and easy observability (in the gateway log every request has the same URL and method). The practical fix: extract the operation name from SOAPAction or the Body root and use it as a metric label, and implement caching at the application layer (see caching).
Answer in layers. Network: TLS only, preferably mTLS with a certificate allow-list. Message: wsse:Security with a short Timestamp, signature over Body and Timestamp, encryption of sensitive fields, and a shared nonce cache. Parser: DOCTYPE banned, external entities off, caps on element depth, element count, text length and attachment size — that closes XXE and entity bombs. Validation: every message checked against the XSD before it reaches your logic. Authorisation: map the identity to roles and enforce them at the operation level, not just the endpoint. Operations: rate limiting, timeouts, a circuit breaker and masked metadata logging. Finish by saying all of this belongs in one shared layer, not in each endpoint.
SOA is an architectural style, not a product: put capability behind an explicit contract, take boundaries seriously, share schema rather than classes. Microservices are the same principles taken to an extreme on team autonomy, with the smart central pipe deliberately removed.
SOAP is a message protocol: an Envelope with a Header (processing metadata, mustUnderstand, role) and a Body (content or a Fault); 1.1 uses text/xml and a SOAPAction header, 1.2 uses application/soap+xml and an action parameter. WSDL is the contract: types/message/portType abstract, binding/service concrete; document/literal wrapped is the practical default. In XSD, remember elementFormDefault="qualified".
In Java: contract-first for every cross-organisation integration; generate with wsdl2java or wsimport (no longer shipped in the JDK since 11); Spring-WS for a new service with your own contract and Apache CXF for heavy WS-*. Build the JAXBContext once, never share a Marshaller, and harden the parser against XXE.
From WS-*: WS-Security provides message-level security that TLS cannot (partial signing, non-repudiation, transport independence); WS-Addressing lifts addressing and correlation out of the transport, and MessageID makes an excellent idempotency key; WS-RM is not a substitute for a broker; MTOM saves binary attachments from base64 bloat.
Finally: do not build new APIs in SOAP, but do not rewrite healthy SOAP either — put a facade in front and replace it incrementally. Do not let an ESB become a god object; implement the EIP patterns in versioned code. And if you move into payments: ISO 8583 is positional and compact, ISO 20022 is structured XML — lossless translation between the two is a full-time job for a lot of banking engineers.