Expose EJB as SOAP Web Services

TL;DR: Learn how to expose a Jakarta Enterprise Beans (EJB) stateless session bean as a SOAP Web Service using JAX-WS on WildFly 41.0.1.Final and Jakarta EE 11. This modern guide covers the jakarta.* namespace migration, secure transport configuration, Apache CXF performance optimizations, and testing tools like curl.

Starting with modern enterprise releases, Jakarta XML Web Services (JAX-WS) 4.0 (part of Jakarta EE 10 and 11) remains the standard specification for developing robust, contract-driven SOAP web services. On WildFly 41.0.1.Final, the reference web services stack is powered by JBossWS-CXF (built on top of Apache CXF), which provides enterprise-grade performance, flexible interceptors, and high-security standards.

Exposing a stateless session bean as a SOAP web service has been a first-class feature since early J2EE specifications. Modern Jakarta EE versions make this approach incredibly streamlined and lightweight. By utilizing simple annotations, you can define, package, and deploy a web service on WildFly in minutes without bloated XML configurations.

This tutorial provides a complete, modern example of exposing a Stateless Session Bean as a SOAP Web Service endpoint, highlighting the architectural choices, security considerations, and deployment lifecycle.

SOAP vs. REST: Why SOAP Still Matters

While Jakarta RESTful Web Services (JAX-RS) are often favored for lightweight, mobile-friendly APIs, SOAP remains highly relevant in enterprise ecosystems. SOAP provides a formal, typed contract via WSDL, which ensures strict compatibility between client and server. Additionally, built-in standards like WS-Security (for message-level encryption and digital signatures) and WS-ReliableMessaging make SOAP indispensable for finance, insurance, and complex B2B system integrations.

What is the Advantage of Exposing an EJB as a Web Service?

Exposing an EJB as a SOAP endpoint offers unique advantages compared to exposing a simple POJO as a web service:

  • Multi-Protocol Access: The bean can be accessed concurrently via SOAP (web service) and via RMI/IIOP (by exposing a @Remote interface), allowing seamless integration for both web and legacy desktop clients.
  • Declarative Transactions and Security: Since the service runs inside the EJB container, it natively inherits declarative transaction boundaries (e.g., @TransactionAttribute) and declarative role-based security (e.g., @RolesAllowed) without requiring extra boilerplate code.
  • Resource Management: The container manages EJB instance pooling, lifecycle hooks, and asynchronous execution transparently.

The following table compares EJB Web Services with POJO Web Services in Jakarta EE 11:

Feature EJB Web Service POJO Web Service
POJO Support Yes Yes
Dependency Injection (CDI / @Inject) Yes Yes
Lifecycle Methods (@PostConstruct) Yes Yes
Declarative Transactions (Out-of-the-box) Yes (via Container-Managed Transactions) No (requires explicit CDI transaction interceptors)
Declarative Security (Out-of-the-box) Yes (via EJB Security Annotations) No (requires security filters/interceptors)

Step-by-Step Implementation

Below is a modern, Jakarta EE 11-compliant stateless session bean exposing a method getPerson that returns a data object Person.

First, let's look at our data class, which uses Jakarta XML Binding (JAXB) to define how the object is serialized to XML:

package com.sample;

import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "Person")
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {

    @XmlElement(required = true)
    private String name;

    @XmlElement(required = true)
    private String surname;

    public Person() {
    }

    public Person(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    // Getters and Setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getSurname() { return surname; }
    public void setSurname(String surname) { this.surname = surname; }
}

Next, we define our local business interface:

package com.sample;

import jakarta.ejb.Local;

@Local
public interface SampleWSLocal {
    Person getPerson(String userId);
}

Finally, we write the Stateless Session Bean annotated as a Web Service Endpoint:

package com.sample;

import jakarta.ejb.Stateless;
import jakarta.jws.WebMethod;
import jakarta.jws.WebParam;
import jakarta.jws.WebService;
import jakarta.jws.soap.SOAPBinding;

@Stateless
@WebService(
    name = "SampleWS",
    serviceName = "SampleWSService",
    targetNamespace = "http://mastertheboss.com/ws"
)
@SOAPBinding(style = SOAPBinding.Style.DOCUMENT, use = SOAPBinding.Use.LITERAL)
public class SampleWS implements SampleWSLocal {

    public SampleWS() {
    }

    @Override
    @WebMethod
    public Person getPerson(@WebParam(name = "userId") String userId) { 
        return new Person("Michael", "Jackson");    
    }
}

The only method exposed is getPerson. When you declare your EJB class with the @WebService annotation, the WildFly container automatically handles the generation of stubs, schemas, and the WSDL (Web Services Description Language) that describes the service contract.

Note: Applying the @WebService annotation directly to the EJB bean class exposes only methods explicitly marked with @WebMethod (or all public methods if no @WebMethod annotations are present). If you place @WebService on the EJB business interface, that interface acts as the formal Service Endpoint Interface (SEI).

All objects transmitted over SOAP are serialized into XML via JAXB. Here is how the serialized SOAP response message looks when retrieving our Person object:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <ns2:getPersonResponse xmlns:ns2="http://mastertheboss.com/ws">
      <return>
        <name>Michael</name>
        <surname>Jackson</surname>
      </return>
    </ns2:getPersonResponse>
  </soap:Body>
</soap:Envelope>

WSDL Location and Verification on WildFly 41

Once you package your application into a standard .war or .ear and deploy it to WildFly, the container exposes the web service endpoint. You can query the WSDL directly through your browser or a command-line tool via the following URL pattern:

http://localhost:8080/YourAppSecurityPath/SampleWSService/SampleWS?wsdl

To verify and inspect your deployed web services directly from the WildFly CLI, execute the following command:

/subsystem=webservices:read-resource(recursive=true)

Security Best Practices for EJB Web Services

Securing enterprise SOAP endpoints requires robust protocols. Do not transmit credentials or sensitive payloads over plain HTTP. Ensure the following security measures are implemented:

  • Transport Level Security (HTTPS/TLS): Configure WildFly's undertow subsystem to enforce SSL/TLS on port 8443. Ensure clients connect via https://.
  • WS-Security (WSS): For message-level security (encryption, decryption, signatures), utilize Apache CXF's WS-Security capabilities. This is achieved by configuring the CXF runtime using custom handlers or WSS4J interceptors within a jboss-webservices.xml deployment descriptor.
  • Avoid Basic Auth over Plain HTTP: Basic authentication transmits user credentials in base64. Always pair basic authentication with active HTTPS to prevent credential interception.

JAX-WS Handlers vs. Apache CXF Interceptors

When you need to audit, log, or modify incoming and outgoing SOAP messages, you have two primary options:

  1. JAX-WS Handlers: These are standard, portable components defined by the Jakarta XML Web Services specification. You can implement a SOAPHandler (to access raw SOAP XML/headers) or a LogicalHandler (to access payload content). They are configured using an XML file referenced via the @HandlerChain annotation on your EJB class.
  2. Apache CXF Interceptors: These are native, phase-based components specific to the WildFly Apache CXF stack. CXF interceptors provide superior performance, low-level stream modification capabilities, and seamless integration with complex security frameworks like WSS4J. They are typically configured programmatically or inside jboss-webservices.xml.

Timeout Management and Performance Tuning

To prevent blocking threads and causing resource exhaustion, configure appropriate connection and read timeouts. In the Apache CXF environment on WildFly, timeouts are configured programmatically on the client-side proxy using the HTTPConduit:

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;

// ... Inside your Client Stub ...
SampleWS port = service.getSampleWSPort();
Client client = ClientProxy.getClient(port);
HTTPConduit http = (HTTPConduit) client.getConduit();

HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
httpClientPolicy.setConnectionTimeout(5000); // 5 seconds connection timeout
httpClientPolicy.setReceiveTimeout(15000);   // 15 seconds read/receive timeout
http.setClient(httpClientPolicy);

Testing Your SOAP Service with Curl

Modern testing has moved away from heavy, outdated Eclipse tooling. Developers prefer automated integration testing with JUnit or quick command-line diagnostics using curl. To test your deployed EJB SOAP service, save your SOAP request envelope inside a file named request.xml:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="http://mastertheboss.com/ws">
   <soapenv:Header/>
   <soapenv:Body>
      <ws:getPerson>
         <userId>123</userId>
      </ws:getPerson>
   </soapenv:Body>
</soapenv:Envelope>

Run the following command to invoke your web service over secure HTTPS:

curl -X POST -H "Content-Type: text/xml; charset=utf-8" -d @request.xml https://localhost:8443/YourApp/SampleWSService/SampleWS

Exception Handling in EJB Web Services

When an error occurs during execution, your EJB should throw a custom application exception annotated with @WebFault. When caught by the JBossWS-CXF engine, this exception is automatically translated into a standard SOAP Fault, preserving structured error metadata while hiding sensitive database/system stack traces from client applications.

Best Practices for EJB Web Services

  • Avoid Huge Object Graphs: XML serialization has a larger footprint than binary protocols or JSON. Avoid returning highly complex, recursive object graphs over SOAP to minimize network serialization overhead.
  • Expose Coarse-Grained Methods: SOAP calls incur network and XML processing overhead. Design your services with "coarse-grained" interfaces (e.g., submitting a complete invoice document) rather than "fine-grained" calls (e.g., individually adding lines to an invoice).
  • Understand Statelessness: EJB Web Services must be Stateless. You cannot expose Stateful Session Beans (SFSB) as web service endpoints because SOAP-based web services are inherently stateless.

Top-down Web Services vs. Bottom-up

Creating SOAP web services can be approached from two directions:

  • Bottom-Up (Code-First): You write your EJB class first and let the JAX-WS runtime automatically generate the WSDL contract. This is the method we used in this tutorial. It is fast, intuitive, and perfect for exposing existing enterprise business layers.
  • Top-Down (Contract-First): You design the formal WSDL first, then generate the Java classes and EJB skeletons from it using tools like wsconsume. This is widely considered the preferred approach for enterprise integrations, as it decouples the service interface from the internal database and programming language specifics.

Frequently Asked Questions (FAQs)

1. Can I use Jakarta EE CDI beans instead of EJB for JAX-WS Web Services on WildFly 41?
Yes, WildFly 41 fully supports exposing plain POJOs with CDI injection as JAX-WS endpoints. However, using CDI means you lose the automatic, declarative transaction handling and declarative role-based security native to EJB session beans unless you explicitly configure transaction/security interceptors.

2. Why did my application fail with ClassNotFoundException on javax.jws.*?
WildFly 41 and Jakarta EE 11 have completely transitioned to the jakarta.* namespace. You must replace all references of javax.jws.*, javax.xml.ws.*, and javax.ejb.* with jakarta.jws.*, jakarta.xml.ws.*, and jakarta.ejb.* in your imports.

3. How can I pass custom headers or implement authentication with JBossWS-CXF?
You can inject the JAX-WS WebServiceContext resource inside your EJB. This context allows you to query incoming SOAP headers, HTTP headers, client IP addresses, and the authenticated Principal directly within your business logic.


Recommended Articles

Call an EJB from another application in WildFly using EJB Lookup

Learn how to invoke EJBs from a remote EJB Client using WildFly's EJB lookup feature. This tutorial covers the steps for creating an EJB server project, setting up security, and configuring dependencies.

EJB to EJB Communication in a Cluster with WildFly 31

Learn how to set up EJB communication between a cluster of WildFly servers and an EJB client, enabling dynamic view of the cluster topology.

Configure Clustered Timer EJB 3 Services on WildFly with Database Storage

Learn how to configure clustered Timer EJB 3 services using a PostgreSQL database in WildFly. #WildFly #JavaEE #ClusteredEJB

EJB to EJB Invocations Between Two WildFly Servers: A Comprehensive Guide

Learn how to perform EJB to EJB invocations between two WildFly server instances, including configuration for standalone and remote clients. #WildFly #JavaEE #CloudNative