Spring Boot + Apache CXF: Building Production-Ready SOAP WebServices with WSDL & WS-Security

This guide covers integrating Spring Boot with Apache CXF to publish and consume SOAP web services, detailing WSDL-first vs code-first approaches, endpoint configuration, client invocation patterns, WS-Security (username tokens, signatures, encryption), MTOM attachments, message validation, exception handling, testing strategies, and production hardening against XXE, large payloads, and version conflicts.

Xiaolin Talks Programming
Xiaolin Talks Programming
Xiaolin Talks Programming
Spring Boot + Apache CXF: Building Production-Ready SOAP WebServices with WSDL & WS-Security

Scenario and Pain Points

SOAP remains unavoidable in banking core, credit, payment, government approval, and ERP integration systems. The contract is fixed (WSDL + XSD), callers may use legacy stacks like Axis1, JAX-WS RI, or .NET ASMX, and changing a single field requires extensive coordination. Typical constraints include rigid contracts, WS-Security requirements (username tokens, signatures, encryption, timestamps, certificate rotation), mandatory attachments via MTOM or SwA, mixed protocol versions (SOAP 1.1/1.2, Document/Literal, RPC/Encoded, wrapped/bare), and legacy systems locked by high upgrade costs.

Common pitfalls when integrating Spring Boot with these systems:

Version conflicts: Spring Boot 3 uses Jakarta EE (jakarta.*) while legacy systems use javax.*; CXF 4 requires package migration.

WSDL-code drift: manual stub or annotation changes cause contract drift over time.

Scattered security policies: each legacy system has its own certificates and password policies.

Message parsing issues: XXE, oversized XML, charset mismatches leading to corruption or OOM.

Manual regression: SoapUI, mocks, contract regression lack automation.

CXF-Spring Boot integration gaps: Bus, Interceptor, Endpoint interaction with auto-configuration and servlet container.

CXF Architecture Overview

Core CXF concepts:

Bus : runtime bus holding extensions, factories, interceptors, features, bindings, transports — CXF's own application context.

Frontend : frontend model; most common is JAX-WS, also supports Simple Frontend and JAX-RS.

Endpoint, Server, Client : server exposes via Endpoint, client calls via Client; Spring commonly uses EndpointImpl and JaxWsProxyFactoryBean.

Interceptor Chain : messages flow through phases — RECEIVE, PRE_STREAM, UNMARSHAL, PRE_INVOKE, POST_INVOKE, MARSHAL, SEND — where logging, security, auditing, encoding conversion are inserted.

Data Binding : default JAXB for Java-XML mapping; also supports Aegis, XMLBeans.

Transport : HTTP, JMS, Local; Spring Boot typically uses embedded Tomcat/Jetty/Undertow for HTTP.

WSDL-first vs Code-first :

WSDL-first: start with WSDL/XSD, generate interfaces and client via cxf-codegen-plugin; preferred for legacy integration, external publishing, strict contracts.

Code-first: write Java with JAX-WS annotations, CXF generates WSDL; suitable for rapid iteration on new internal services, but freeze WSDL once exposed externally.

Practical choice: WSDL-first for legacy/external; code-first for internal new services, but solidify WSDL quickly.

The cxf-spring-boot-starter-jaxws auto-configures CXFServlet, SpringBus, Bus, and servlet mapping; developers mainly configure Endpoint and client beans.

Service Publishing: Exposing WSDL in Spring Boot

Dependencies and Version Selection

<!-- Spring Boot 3.x + Jakarta -->
<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
  <version>4.0.4</version>
</dependency>

<!-- Spring Boot 2.7 + javax for legacy -->
<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
  <version>3.5.7</version>
</dependency>

Use Spring Boot 2.7 + CXF 3.5.x if legacy systems run on Java 8/11 and depend on javax.jws; new systems go Spring Boot 3 + CXF 4. The API difference is mainly package rename from javax.* to jakarta.* — critical not to miss during migration.

Minimal configuration:

cxf:
  path: /services
  servlet:
    load-on-startup: 1

Published WSDL URL pattern: http://localhost:8080/services/order/v1?wsdl.

Interface Definition

@WebService(
  targetNamespace = "http://example.com/order/v1",
  name = "OrderService",
  serviceName = "OrderService",
  portName = "OrderServicePort"
)
public interface OrderService {
  @WebMethod(operationName = "queryOrder")
  @WebResult(name = "OrderResponse", targetNamespace = "http://example.com/order/v1")
  OrderResponse queryOrder(
    @WebParam(name = "request", targetNamespace = "http://example.com/order/v1")
    OrderQuery request
  ) throws OrderFault;
}

Implementation:

@WebService(
  endpointInterface = "com.example.order.OrderService",
  targetNamespace = "http://example.com/order/v1"
)
@Service
public class OrderServiceImpl implements OrderService {
  @Override
  public OrderResponse queryOrder(OrderQuery request) throws OrderFault {
    // business logic
  }
}

Endpoint Configuration

@Configuration
public class CxfConfig {
  @Bean
  public SpringBus cxfBus() {
    SpringBus bus = new SpringBus();
    // register logging, security, audit features and interceptors
    return bus;
  }

  @Bean
  public Endpoint orderEndpoint(SpringBus bus, OrderServiceImpl orderService) {
    EndpointImpl endpoint = new EndpointImpl(bus, orderService);
    endpoint.publish("/order/v1");
    return endpoint;
  }
}

Multiple endpoints can be mounted on different paths for version isolation; avoid stuffing all interfaces under one path.

WSDL Exposure and Version Management

URL versioning : /order/v1, /order/v2 — simple, Nginx-friendly.

Namespace versioning : http://example.com/order/v1, .../v2 — suits long-term compatibility at same address.

Compatibility rules : only add operations or optional elements; never delete operations, change existing element types/namespaces, or reorder elements.

WSDL freezing : commit WSDL to version control; CI compares old vs new WSDL to prevent contract drift.

Legacy address preservation : keep old URLs via Nginx reverse proxy to new version paths.

Client Invocation

Static Stub Generation (Recommended)

Use cxf-codegen-plugin to generate client code from WSDL:

<plugin>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-codegen-plugin</artifactId>
  <version>4.0.4</version>
  <executions>
    <execution>
      <id>generate-sources</id>
      <phase>generate-sources</phase>
      <configuration>
        <wsdlOptions>
          <wsdlOption>
            <wsdl>src/main/resources/wsdl/order.wsdl</wsdl>
            <extraargs>
              <extraarg>-client</extraarg>
              <extraarg>-p</extraarg>
              <extraarg>com.example.order.client</extraarg>
            </extraargs>
          </wsdlOption>
        </wsdlOptions>
      </configuration>
      <goals>
        <goal>wsdl2java</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Generates OrderService and OrderServiceService — type-safe, preferred for production.

JAX-WS Proxy

@Bean
public OrderService orderService() {
  JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
  factory.setServiceClass(OrderService.class);
  factory.setAddress("https://legacy.example.com/order/v1");

  OrderService port = (OrderService) factory.create();

  Client client = ClientProxy.getClient(port);
  HTTPConduit conduit = (HTTPConduit) client.getConduit();
  HTTPClientPolicy policy = new HTTPClientPolicy();
  policy.setConnectionTimeout(3000);
  policy.setReceiveTimeout(10000);
  conduit.setClient(policy);

  return port;
}

Proxy instances are generally thread-safe singletons, but avoid mutating BindingProvider request context in request threads.

Dynamic Client

JaxWsDynamicClientFactory factory = JaxWsDynamicClientFactory.newInstance();
Client client = factory.createClient("https://legacy.example.com/order/v1?wsdl");
Object[] result = client.invoke("queryOrder", request);

Dynamic client suits frequently changing WSDLs with low call volume. It depends on wsdl4j and compilation tools, has poor runtime parsing performance, and lacks type safety. Avoid in production if static stubs are feasible.

Interceptors

Logging : CXF 3.2+ uses LoggingFeature; older versions use LoggingInInterceptor / LoggingOutInterceptor.

Custom : extend AbstractPhaseInterceptor<Message> or AbstractSoapInterceptor, insert logic in RECEIVE, PRE_INVOKE, etc.

Correlation ID : client writes SOAPHeader in Out interceptor; server reads in In interceptor and puts into MDC for distributed tracing.

public class TraceOutInterceptor extends AbstractSoapInterceptor {
  public TraceOutInterceptor() {
    super(Phase.WRITE);
  }

  @Override
  public void handleMessage(SoapMessage message) {
    String traceId = MDC.get("traceId");
    // write into SOAP Header
  }
}

Timeouts, Retry, and Connection Pooling

Timeouts : HTTPClientPolicy.setConnectionTimeout and setReceiveTimeout in milliseconds.

Retry : CXF's FailoverFeature supports multi-address failover; business layer can use Spring Retry for idempotent retries — never retry non-idempotent operations blindly.

Connection pooling : CXF defaults to HttpURLConnection (no pool). Add cxf-rt-transports-http-hc5 to use Apache HttpClient 5's PoolingHttpClientConnectionManager.

@Bean
public HTTPConduitConfigurer httpConduitConfigurer() {
  return (name, address, conduit) -> {
    conduit.getClient().setConnectionTimeout(3000);
    conduit.getClient().setReceiveTimeout(10000);
    if (conduit instanceof HttpClientHTTPConduit) {
      // configure pool, TLS, proxy, etc.
    }
  };
}

WS-Security

Dependency:

<dependency>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-rt-ws-security</artifactId>
</dependency>

Username Token

Client:

Map<String, Object> outProps = new HashMap<>();
outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
outProps.put(WSHandlerConstants.USER, "app-user");
outProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_DIGEST);
outProps.put(WSHandlerConstants.PW_CALLBACK_REF, new ClientPasswordCallback());

WSS4JOutInterceptor out = new WSS4JOutInterceptor(outProps);
((BindingProvider) port).getRequestContext()
  .put(WSS4JOutInterceptor.class.getName(), out);

Server:

Map<String, Object> inProps = new HashMap<>();
inProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
inProps.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT);
inProps.put(WSHandlerConstants.PW_CALLBACK_REF, new ServerPasswordCallback());

endpoint.getInInterceptors().add(new WSS4JInInterceptor(inProps));

Password callback returns password based on WSPasswordCallback.getIdentifier(). Production should use PasswordDigest with timestamps to prevent replay. Plaintext passwords are allowed only if counterpart requires.

Signature and Encryption

Configured via WSHandlerConstants.SIGNATURE_PROP, ENCRYPTION_PROP, and WSS4J Merlin Crypto. Note CXF 3.5/4 uses WSS4J 2.x/3.x with prefix org.apache.wss4j.crypto (not legacy org.apache.ws.security.crypto):

org.apache.wss4j.crypto.provider=org.apache.wss4j.common.crypto.Merlin
org.apache.wss4j.crypto.merlin.keystore.type=JKS
org.apache.wss4j.crypto.merlin.keystore.file=classpath:keystore/client.jks
org.apache.wss4j.crypto.merlin.keystore.password=changeit

Typical action combinations:

Client: sign + encrypt + timestamp

Server: verify signature + decrypt + validate timestamp

Algorithms: prefer RSA-SHA256, AES-256; avoid SHA1 and 3DES.

Certificate rotation: use dual-certificate strategy — place both old and new certs in truststore, switch signing first, then encryption, finally retire old cert. Reverse order breaks callers.

Timestamp and MTOM Attachments

Timestamp:

outProps.put(WSHandlerConstants.ACTION,
  WSHandlerConstants.TIMESTAMP + " " + WSHandlerConstants.SIGNATURE);

MTOM service:

@WebService
@MTOM
@BindingType(SOAPBinding.SOAP11HTTP_MTOM_BINDING)
public class FileService {
  public void upload(@XmlMimeType("application/octet-stream") DataHandler file) {
    // stream processing
  }
}

Client enable MTOM:

((BindingProvider) port).getRequestContext().put("mtom-enabled", true);

MTOM avoids Base64 bloat, reducing memory. However, legacy clients may not support xmime:expectedContentTypes; regression test before production rollout.

Message Governance

XML Validation and Namespaces

Enable schema validation on server:

@SchemaValidation(type = SchemaValidationType.IN)
@WebService(endpointInterface = "com.example.order.OrderService")
public class OrderServiceImpl implements OrderService { ... }

Define namespaces centrally in package-info.java:

@XmlSchema(
  namespace = "http://example.com/order/v1",
  elementFormDefault = XmlNsForm.QUALIFIED
)
package com.example.order;

Legacy systems often use elementFormDefault="unqualified". Don't modify generated code directly; prefer JAXB binding files ( -b) or package-level @XmlSchema corrections. wsdl2java -xjc passes args to XJC but cannot solve all namespace issues; forcing it may corrupt generated code.

Exception Mapping

Define business faults with @WebFault for stable SOAP Fault structure:

@WebFault(name = "OrderFault", targetNamespace = "http://example.com/order/v1")
public class OrderFault extends Exception {
  private final OrderFaultDetail detail;
  public OrderFault(OrderFaultDetail detail) {
    super(detail.getMessage());
    this.detail = detail;
  }
  public OrderFaultDetail getFaultInfo() { return detail; }
}

Uncaught runtime exceptions become SOAPFault. Production should use FaultListener or custom FaultOutInterceptor to unify error codes — never expose stack traces to callers.

Log Masking and Auditing

LoggingFeature

supports sensitive element masking:

LoggingFeature logging = new LoggingFeature();
logging.setPrettyLogging(true);
logging.setSensitiveElementNames(Set.of("password", "idCard", "bankCard"));
logging.setSensitiveProtocolHeaderNames(Set.of("Authorization"));
bus.getFeatures().add(logging);

Auditing via custom SOAP interceptor: log request time, source, operation, latency, result code to audit table. Audit logs must also mask passwords and ID numbers.

Testing and Integration

SoapUI and Mock Services

SoapUI/ReadyAPI : import WSDL, auto-generate request templates, XPath assertions, security testing, mock service.

WireMock : map SOAP responses for client unit tests, simulate timeouts, faults, abnormal HTTP status.

CXF Mock : use ServerFactoryBean to spin up minimal local service without external dependencies.

Interface Mock : Mockito mock generated Port interface — fastest but doesn't validate XML contract; suitable for business logic tests.

Contract Regression and WSDL Compatibility Checks

Commit WSDL and XSD to Git; CI compares differences.

Rules: no operation deletion, no type/namespace changes on existing elements, no element reordering; only add optional elements or new operations.

Re-run wsdl2java to regenerate stubs and compile, ensuring old client code unaffected.

SOAP 1.1/1.2, MTOM toggle, security policy changes require end-to-end regression; unit tests alone insufficient.

Production Hardening

Thread Safety

Bus

, Endpoint, client proxies are generally thread-safe singletons.

Custom interceptors must not hold mutable shared state — causes data leakage under concurrency. BindingProvider.getRequestContext() concurrent modification can interfere; configure at startup, avoid runtime changes in request threads.

Security interceptors like WSS4JOutInterceptor with mutable config should be instantiated per client instance, not shared.

XXE Protection

Disable DTD and external entities:

System.setProperty("javax.xml.stream.supportDTD", "false");
System.setProperty("javax.xml.stream.isSupportingExternalEntities", "false");
System.setProperty("org.apache.cxf.stax.allowInsecureParser", "0");

Enable schema validation to reject non-conforming messages. Never parse external WSDLs at runtime and execute arbitrary code from them.

Large Message Memory

Prefer MTOM, DataHandler, Source, Streaming to avoid DOM/JAXB full binding of huge XML.

Limit XML depth, attribute count, element count to prevent Billion Laughs and deep nesting attacks.

Tune JVM heap and direct memory; monitor GC; stream attachments, avoid full byte[] residency.

Charset and Version Compatibility

Standardize on UTF-8; HTTP Content-Type charset must match XML declaration.

For GBK/GB2312 legacy systems, force encoding via interceptors to prevent corruption. javax and jakarta cannot mix; migrate javax.jws, javax.xml.ws, javax.xml.bind simultaneously.

SOAP 1.1 and 1.2 bindings cannot mix; @BindingType must be explicit.

RPC/Encoded style has limited CXF support. For Axis1 legacy services, may need CXF downgrade or custom DataBinding; default config won't suffice.

Closing Remarks

Spring Boot with Apache CXF isn't a trendy architecture, but it's a stable path for integrating legacy SOAP systems. Success hinges not on writing @WebService but on: governing WSDL contracts (version control, CI compatibility checks), unifying WS-Security policies (shared client config, certificate rotation drills), anticipating legacy quirks, pushing logging masking/auditing/exception mapping/schema validation into interceptors, testing beyond business logic (contract regression, end-to-end), and having concrete mitigations for XXE, large payloads, charset issues, thread safety, and javax/jakarta compatibility. Master these, and Spring Boot + CXF runs reliably in banking, government, ERP scenarios without constant firefighting.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javabackend developmentSpring BootWeb ServicesSOAPWSDLApache CXFWS-Security
Xiaolin Talks Programming
Written by

Xiaolin Talks Programming

Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.