Expose EJB as SOAP Web Services
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
@Remoteinterface), 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
undertowsubsystem to enforce SSL/TLS on port8443. Ensure clients connect viahttps://. - 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.xmldeployment 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:
- 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 aLogicalHandler(to access payload content). They are configured using an XML file referenced via the@HandlerChainannotation on your EJB class. - 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