If you wish to test out any of the services mentioned for yourself, there are two ways to do so. Simply clone the git repository, run a maven build, and deploy it to a EJB3-enabled container of your choice (some assembly may be required for your container of choice). I have supplied a web interface which can be reached at http://
Note that I am using WSDL version 1.1, not the newer 1.2/2.0 due to the general lack of adoption and tool support.
Contract-First or Code-First?
I was hoping to avoid this question when I undertook writing this article since I'm not interested in taking part in a holy war. I wanted to use a code-first approach for my examples simply because I am more comfortable defining my service contract in code. There are several tools available for generating a wsdl from service code, with Apache CXF likely emerging as the best of those tools, though it does much more than that. I completed the JEE version of this service using this approach quite effectively. Then I turned my attention to Spring-WS only to be greeted with the following statement in their reference documentation (http://docs.spring.io/spring-ws/docs/2.2.0.RELEASE/reference/htmlsingle/#why-contract-first):
When creating Web services, there are two development styles: Contract Last and Contract First. When using a contract-last approach, you start with the Java code, and let the Web service contract (WSDL, see sidebar) be generated from that. When using contract-first, you start with the WSDL contract, and use Java to implement said contract.
Spring-WS only supports the contract-first development style, and this section explains why.
This seems like an odd place for Spring to choose to take a stance on how something must be done, when from what I've seen they are usually aggressively flexible. However, let it be known that in my examples I used a code-first approach for the JEE implementation. I then lifted the generated schema in that wsdl to generate the Spring wsdl so I could come as close to a code-first approach as possible. While not the most correct of approaches, I believed I was able to achieve my goal.
After I was finished, it was brought to my attention that I wasn't truly comparing apples to apples. To that end, in my github project I have shown how to configure a maven project to use the CXF codegen plugin to build a service implementation from a static wsdl. If you run 'mvn generate-sources' on the fwc-ejb-web module, the generated classes will appear in the target/generated-sources folder. You would then update the service implementation class to add your implementation to any service methods that were stubbed out there. On the flip-side of that coin, I have also set up the cxf java2wsdl plugin to show an example of how you can generate a wsdl from an implementation. I'm not using the results of either of these, but hopefully they can be educational.
Taking advantage of JBoss
I'm going to be taking advantage of some of the features offered by the JBoss Application Server so, as promised, I intend to highlight those features to prevent skewing the analysis toward one framework or the other. Specifically, in my EJB3 annotated SOAP services, I'm letting the container generate my wsdl for me. It does this by using the aforementioned Apache CXF under the hood.
Alternatively, it is possible to generate your own wsdl and host it. As I mentioned earlier, my project in github shows an example of how this would be done in the plugin configuration for the fwc-ejb-web module. During the generate-resources phase it generates a wsdl from a service implementation and places it in the generated-resources directory. Then, during the compile phase you could copy that wsdl into the WEB-INF directory via the maven resources plugin. It can be referenced from there in the wsdlLocation attribute of the @WebService annotation on the service implementation. I left the sample generation there for instructive purposes, but I'm not using it.
Another way JBoss is also helping me out by rewriting the soap:address in the wsdl when it is requested. This is done by setting
EJB3 / JAX-WS SOAP Services
For a SOAP service created with JAX-WS annotations, no real configuration is necessary. For my JBoss EAP container, I only need to have the webservices sub-system enabled which uses Apache CXF to publish your service endpoints. Your classpath will be scanned for any classes annotated with @WebService and, in my case, also generates the wsdl as I mentioned above. Here is my service implementation.
SOAP Service Implementation
//snip - not all imports shown here import javax.ejb.Stateless; import javax.inject.Inject; import javax.jws.WebMethod; import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; @Stateless @WebService(name = "InstitutionService", serviceName = "soap-institution", targetNamespace = InstitutionSoapService.TARGET_NAMESPACE) public class InstitutionSoapServiceImpl implements InstitutionSoapService { @Inject private InstitutionService service; @Override @WebMethod @WebResult(targetNamespace = InstitutionSoapService.TARGET_NAMESPACE, name = FindInstitutionsResponse.NAME) public FindInstitutionsResponse findInstitutions( @WebParam( targetNamespace = InstitutionSoapService.TARGET_NAMESPACE, name = FindInstitutionsRequest.NAME) FindInstitutionsRequest request) { FindInstitutionsResponse response = new FindInstitutionsResponse(); // snip return response; } }
A breakdown of the annotations:
- The @Stateless annotation marks this as a stateless session bean
- The @WebService annotation is required on any Service Endpoint Implementation or endpoint interface. It informs the container that it is a JAX-WS endpoint.
- The 'name' attribute specifies the name of the web service and is used as the name of the portType in the wsdl.
- The 'serviceName' attribute specifies the service name of this web service and is used as the name of the wsdl:service in the wsdl. It is also used as the url prefix to access methods defined on this service: /
/fwc-ejb/soap-institution/InstitutionService - The 'targetNamespace' attribute specifies the namespace of the service which must match the namespace on any incoming requests
- The @Inject annotation comes from CDI and injects a @RequestScoped service bean for me as discussed in my Rest Service article and is not relevant to the subject at hand
- The @WebMethod annotation marks a method to be exposed on a @WebService endpoint.
- The @WebResult annotation describes the SOAP response, which in this case is the FindInstitutionsResponse
- The 'name' attribute specifies the name of the xml element for the response object
- The 'targetNamespace' attribute specifies the namespace of the xml element for the response object
- The @WebParam annotation describes the expected SOAP request, which will be matched against any incoming requests on this endpoint. The 'name' and 'targetNamespace' attributes work exactly the same for this annotation as they do for the @WebResult annotation. If the incoming request does not contain the exact same elements and namespaces, the request will be rejected.
There is slightly more to it yet, as I still need to define my request and response objects. If you're looking for them in the code, you'll find them in the fwc-common module. When a request comes in, the XML request will be unmarshaled to the FindInstitutionsRequest object, and the response XML is obtained by marshaling the FindInstitutionsResponse back to XML. The names and namespaces of the elements is specified via JAXB annotations. They are fairly self-explanatory so I won't go into detail describing them.
Transfer Objects
@XmlRootElement(namespace = "http://fwc.gitter.org/services/") @XmlType(name = "findInstitutionsRequest") public class FindInstitutionsRequest { public static final String NAME = "findInstitutionsRequest"; private String keyword; @XmlElement(name = "keyword") public String getKeyword() { return keyword; } public void setKeyword(String keyword) { this.keyword = keyword; } } //---------------------------------------------------------------- @XmlRootElement(namespace = "http://fwc.gitter.org/services/") @XmlType(name = "institutionList") public class FindInstitutionsResponse { public static final String NAME = "institutionList"; private List institutions; @XmlElement(name = "institution") public List getInstitutions() { return institutions; } public void setInstitutions(List institutions) { this.institutions = institutions; } } //---------------------------------------------------------------- public class Institution { private Map<String, String=""> institutionData; @XmlElement public String getAddress() { // I'm cheating - normally you wouldn't do this in a transfer object, // but I wanted the SOAP response to be aesthetically pleasing and // haven't implemented a domain layer. return new StringBuilder() .append(institutionData.get("ADDR")).append(" ") .append(institutionData.get("CITY")).append(", ") .append(institutionData.get("STABBR")).append(" ") .append(institutionData.get("ZIP")).toString(); } @XmlTransient public Map<String, String=""> getInstitutionData() { return institutionData; } @XmlElement public String getInstitutionName() { return institutionData.get("INSTNM"); } public void setInstitutionData(Map<String, String=""> institutionData) { this.institutionData = institutionData; } }
As an example, if I send this request:
SOAP Request
<!-- HTTP headers omitted --> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <fwc:findInstitutions xmlns:fwc="http://fwc.gitter.org/services/"> <fwc:findInstitutionsRequest> <keyword>Milwaukee WI</keyword> </fwc:findInstitutionsRequest> </fwc:findInstitutions> </soap:Body> </soap:Envelope>
I should receive a response like this:
SOAP Response
<!-- HTTP headers omitted --> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ns2:findInstitutionsResponse xmlns:ns2="http://fwc.gitter.org/services/"> <ns2:institutionList> <institution> <address>500 Silverspring Rd Ste K340 Glendale, WI 53217</address> <institutionName>Bryant & Stratton College-Bayshore</institutionName> </institution> <institution> <address>10950 W Potter Road Wauwatosa, WI 53226</address> <institutionName>Bryant & Stratton College-Wauwatosa</institutionName> </institution> <institution> <address>4425 N Port Washington Rd Glendale, WI 53212</address> <institutionName>Columbia College of Nursing</institutionName> </institution> <!-- snip ... --> </ns2:institutionList> </ns2:findInstitutionsResponse> </soap:Body> </soap:Envelope>
If you have deployed the application, the wsdl will be visible at URL http://
Spring SOAP Services with Spring-WS
For this example, I'm using spring-ws (2.2.0). As opposed to the EJB3 and JAX-WS method, which is ultimately published by Apache CXF in JBoss, Spring SOAP Services are published by the Spring Container. This also means that the JBoss webservices subsystem is completely unaware of my deployed Spring-WS soap services. In other words, I can't depend on JBoss to generate my wsdls, or rewrite the soap:address for me as above automagically. The first thing we have to do is add a mapping for the Soap requests to the spring-ws dispatcher servlet in the web.xml deployment descriptor.
web.xml
<web-app> <!-- snip --> <servlet> <servlet-name>webservices</servlet-name> <servlet-class> org.springframework.ws.transport.http.MessageDispatcherServlet </servlet-class> <init-param> <param-name>transformWsdlLocations</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>webservices</servlet-name> <url-pattern>/soap/*</url-pattern> </servlet-mapping> </web-app>
The transformWsdlLocations parameter informs Spring-WS that we wish for the soap:address to be rewritten to the current binding address. This performs the same job for us as the JBoss webservices subsystem as laid out above. In addition to mapping the SpringWS dispatcher servlet, we will have to configure the application context.
applicationContext.xml
<beans> <!-- snip --> <sws:annotation-driven /> <sws:dynamic-wsdl id="InstitutionService" portTypeName="InstitutionService" serviceName="InstitutionService" locationUri="/soap/institution/InstitutionService"> <sws:xsd location="/schemas/InstitutionService.xsd" /> </sws:dynamic-wsdl> </beans>
The
That is enough to configure a simple web service. The rest of the configuration is in the service implementation itself.
SOAP Service Implementation
/* snip - not all imports shown */ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ws.server.endpoint.annotation.Endpoint; import org.springframework.ws.server.endpoint.annotation.PayloadRoot; import org.springframework.ws.server.endpoint.annotation.RequestPayload; import org.springframework.ws.server.endpoint.annotation.ResponsePayload; @Endpoint public class InstitutionSoapServiceImpl implements InstitutionSoapService { private InstitutionService service; @Autowired public InstitutionSoapServiceImpl(InstitutionService service) { this.service = service; } @Override @PayloadRoot(localPart = "findInstitutionsRequest", namespace = InstitutionSoapService.TARGET_NAMESPACE) public @ResponsePayload FindInstitutionsResponse findInstitutions( @RequestPayload FindInstitutionsRequest request) { FindInstitutionsResponse response = new FindInstitutionsResponse(); /* snip */ return response; } }
Here is a breakdown of the annotations:
- The @Endpoint annotation acts similarly to the @Component annotation, marking this class a service endpoint implementation and is picked up thanks to the sws:annotation-driven configuration element. It is analagous to the @WebService annotation.
- The @Autowired annotation configures the injection point for the context InstitutionService bean
- The @PayloadRoot annotation is the counterpart to the JAX-WS @WebMethod annotation. Similarly, it sets up the namespace and name of the expected type for a request.
- The @ResponsePayload annotation indicates that the method's return value should be bound to the response payload.
- The @RequestPayload annotation indicates that a method parameter should be bound to the request payload.
As you may have noticed, this service uses the same transfer objects as it's EJB3/JAX-WS cousin. It is therefore using the same JAXB configuration for marshaling the request and response payloads. This, then, is all that is needed. Here is an example request and response for this Spring-WS service.
Request
<!-- HTTP Headers omitted --> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <fwc:findInstitutionsRequest xmlns:fwc="http://fwc.gitter.org/services/"> <fwc:keyword>Milwaukee WI</fwc:keyword> </fwc:findInstitutionsRequest> </soap:Body> </soap:Envelope>
Response
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header/> <SOAP-ENV:Body> <ns3:findInstitutionsResponse xmlns:ns3="http://fwc.gitter.org/services/" xmlns=""> <institution> <address>500 Silverspring Rd Ste K340 Glendale, WI 53217</address> <institutionName>Bryant & Stratton College-Bayshore</institutionName> </institution> <institution> <address>10950 W Potter Road Wauwatosa, WI 53226</address> <institutionName>Bryant & Stratton College-Wauwatosa</institutionName> </institution> <institution> <address>4425 N Port Washington Rd Glendale, WI 53212</address> <institutionName>Columbia College of Nursing</institutionName> </institution> <!-- Snip ... --> </ns3:findInstitutionsResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
Analysis
There are some notable things missing from this article, including some of the reasons that would compel you to use SOAP services instead of REST in the first place. This includes the entire WS-* stack: reliable messaging, use of transports other than HTTP, support for signed/encrypted/authenticated service calls, and transactionality of service calls. I decided to break these concepts out into separate articles so I could do them justice. I'm not sure the gap between EJB3/JAX-WS and Spring-WS is wide enough for the configuration of a simple SOAP service to enable us to make a decision on this alone. However, I did notice a couple things that are at least worth pointing out.
First, I have a confession. When I initially wrote the code for the Spring-WS service, I spent several frustrated hours trying to hammer out a working service, specifically when trying to write up a service implementation that would work with the WSDL that I had cobbled together. My frustration was really due to two things. First, I chose not to do "Contract-First" service development, which would have, more or less, guaranteed that the implementation would have matched the WSDL. Second, my lack of experience with Spring-WS configuration had me battling myself for the first few hours. When I finished getting the service to work, I went through several more passes to understand how the moving parts of Spring-WS were working together and slowly trimmed my configuration down to just the pieces that were necessary to make it work, discovering that some of what I had done was unnecessary or redundant. So hopefully my analysis remains untainted from my early frustrations.
I noticed that configuring the URL of the endpoint in Spring appeared to be more flexible. This may differ slightly when using other containers, but the endpoint of my EJB3/JAX-WS service was /
So which is better? There really weren't any notable differences in the two versions of the implementation. While Spring-WS is certainly a viable option, I was very put off by the fact that my desired Code-First approach wasn't supported. This alone might make me choose to go with JEE. In fact, if you choose to use Spring for other reasons, it is possible to plug in Apache CXF to publish your Soap Services and host the JAX-WS service implementation in the first example via CXF in the Spring container. Honestly, I wasn't expecting to notice any great divergence yet, but I anticipate that I will begin to see some major differences as I delve deeper down the rabbit hole.
No comments:
Post a Comment