Update
Updated example application to use annotations for WS endpoint resolution
Spring Webservices encourages a contract first, message oriented approach to creating Webservices. The underlying details are completely under developer control starting from the contract to the marshalling/unmarshalling details to the endpoint handling the request. The same holds for the REST support that was just recently added to Spring MVC (Spring 3.0).
Traditionally, contract first service design has involved creating XSDs defining the messages, however using the Schema Generator (SchemaGen) found in java 6, contracts can be defined in JAXB instead.
Let us start by an example of a simple service to expose as a WS and REST web service – call it the MemberService. MemberService exposes one operation “get member details” which returns the details of a member/person, given an identifier.
We start by defining the contract using standard JAXB 2 annotations on the request:
// MemberDetailsRequest.java @XmlRootElement(name = "MemberDetailsRequest", namespace = MemberDetailsNamespace.NAMESPACE) @XmlType(name = "MemberDetailsRequest", namespace = MemberDetailsNamespace.NAMESPACE) @XmlAccessorType(XmlAccessType.FIELD) public class MemberDetailsRequest { public MemberDetailsRequest() { } public MemberDetailsRequest(String id) { this.id = id; } @XmlElement(required = true) private String id; public String getId() { return id; } public void setId(String id) { this.id = id; } }
The response is defined in a similar fashion.
Based on the messages, an XSD is generated using the SchemaGen Ant task defined in the Maven pom.xml:
<taskdef name="schemagen" classname="com.sun.tools.jxc.SchemaGenTask" /> <schemagen srcdir="src/main/java" destdir="src/main/resources" includes="**/*Request.java,**/*Response.java"> <schema namespace="http://open.bekk.no/memberservice" file="memberservice-common.xsd" /> </schemagen>
The generated memberservice-common.xsd looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <xs:schema version="1.0" targetNamespace="http://open.bekk.no/memberservice" xmlns:tns="http://open.bekk.no/memberservice" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="MemberDetailsRequest" type="tns:MemberDetailsRequest"/> <xs:element name="MemberDetailsResponse" type="tns:MemberDetailsResponse"/> <xs:complexType name="MemberDetailsRequest"> <xs:sequence> <xs:element name="id" type="xs:string"/> </xs:sequence> </xs:complexType> <xs:complexType name="MemberDetailsResponse"> <xs:sequence> <xs:element name="memberdetail" type="tns:MemberDetailType" form="qualified"/> </xs:sequence> </xs:complexType> <xs:complexType name="MemberDetailType"> <xs:sequence> <xs:element name="name" type="xs:string" form="qualified"/> <xs:element name="phone" type="xs:string" form="qualified"/> <xs:element name="city" type="xs:string" form="qualified"/> <xs:element name="state" type="xs:string" form="qualified"/> </xs:sequence> </xs:complexType> </xs:schema>
Next, we define a web service endpoint, that will handle all WS requests. This endpoint is annotated using Spring’s @Endpoint annotation:
// MemberDetailsEndpoint.java @Endpoint public class MemberDetailsEndpoint { private MemberManager memberManager; @PayloadRoot(localPart = "MemberDetailsRequest", namespace = MemberDetailsNamespace.NAMESPACE) public MemberDetailsResponse memberDetails(MemberDetailsRequest request) { MemberDetail memberDetail = memberManager.getMemberDetails(request .getId()); MemberDetailsResponse response = new MemberDetailsResponse(memberDetail); return response; } public void setMemberManager(MemberManager memberManager) { this.memberManager = memberManager; } }
Next, we define a controller that will handle the REST requests. This class too requires a JAXB marshaller to be available, since it uses the @ResponseBody annotation and is returning a JAXB annotated class:
// MemberDetailsController.java @Controller public class MemberDetailsController { private MemberManager memberManager; @RequestMapping(value = "/member_details/{id}", method = RequestMethod.GET) @ResponseBody public MemberDetailsResponse memberDetails( @PathVariable(value = "id") String id) { MemberDetail detail = memberManager.getMemberDetails(id); MemberDetailsResponse response = new MemberDetailsResponse(detail); return response; } public void setMemberManager(MemberManager memberManager) { this.memberManager = memberManager; } }
The last thing to do is to wire it all together in Spring configuration files and web.xml. Let’s start by defining the latter:
<servlet> <servlet-name>ws</servlet-name> <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet> <servlet-name>rest</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ws</servlet-name> <url-pattern>/ws/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ws</servlet-name> <url-pattern>*.wsdl</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>rest</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping>
All request to *.wsdl and /ws/* will now be handled by the MessageDsipatcherServlet provided with Spring WS. Requests for /rest/* are handled by a standard Spring MVC DispatcherServlet.
Next, let’s look at the Spring configuration files. Common elements like the MemberManager and Jaxb2Marshaller required by both the WS web service and the REST web service go in the application-context.xml file. Config that is only intended for the WS web service, is found in the ws-servlet.xml file; similarly config intended for the REST web service, is found in the rest-servlet.xml file. Both file names are determined by Spring at runtime based on the value of the tag found in web.xml.
Let’s first have a look at ws-servlet.xml:
<bean class="no.bekk.open.memberservice.endpoint.MemberDetailsEndpoint"> <property name="memberManager" ref="memberManager" /> </bean> <bean class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping"> <description>An endpoint mapping strategy that looks for @Endpoint and @PayloadRoot annotations.</description> </bean> <bean class="org.springframework.ws.server.endpoint.adapter.MarshallingMethodEndpointAdapter"> <description>Enables the MessageDispatchServlet to invoke methods requiring OXM marshalling.</description> <property name="marshaller" ref="jaxb2Marshaller" /> <property name="unmarshaller" ref="jaxb2Marshaller" /> </bean> <bean id="MemberDetails" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition"> <property name="schema"> <bean class="org.springframework.xml.xsd.SimpleXsdSchema"> <property name="xsd" value="classpath:/memberservice-common.xsd" /> </bean> </property> <property name="portTypeName" value="memberservice" /> <property name="serviceName" value="memberservice" /> <property name="locationUri" value="http://localhost:8080/memberservice/ws/MemberDetailsRequest" /> <property name="soapActions"> <map> <entry key="MemberDetails" value="http://open.bekk.no/memberservice/MemberDetails" /> </map> </property> </bean>
The MemberDetails bean automatically generates a WSDL based on conventions, and after you start the jetty server using mvn jetty:run, the wsdl can be accessed at: http://localhost:8080/memberservice/MemberDetails.wsdl. This is great news if you dislike writing XSDs and WSDLs as much as I do! Now in addition, you can actually run your WS web service by importing the WSDL in a tool like SOAP UI – the SOAP address location defined in the WSDL should work just fine.
As you can imagine, the enpoint mapping is defined by the PayloadRootAnnotationMethodEndpointMapping bean, which will make sure all MemberDetailsRequests are handled by our memberDetailsEndpoint bean. And, as you can see, the MarshallingMethodEndpointAdapter uses the previously defined JAXB marshaller to handle (un)marshalling of the messages.
Now, let’s move over to the REST web service, and its config that is found in rest-servlet.xml:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <ref bean="marshallingHttpMessageConverter" /> </list> </property> </bean> <bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter"> <property name="marshaller" ref="jaxb2Marshaller" /> <property name="unmarshaller" ref="jaxb2Marshaller" /> </bean>
Since we’re using the @ResponseBody annotation and are returning a JAXB annotated class, we need to define a AnnotationMethodHandlerAdapter that will take care of converting the messages using the marshallingHttpMessageConverter that uses the previously defined jaxb2Marshaller for marshalling messages. That’s about it. Point you browser at http://localhost:8080/memberservice/rest/member_details/1 to call the REST based member service.
As you can see, we created a truly contract first WS and REST web service without writing a single line of XSD or WSDL! In addition, we ended up exposing the very same service using two different interfaces (WS and REST) – this indeed is DRY.
Final note
The source code for this application can be downloaded from: http://github.com/landro/memberservice
I’d also like to thank Biju Kunjummen for writing and excellent article on How to use Spring Web Services and letting me base my sample application on his code.
Pingback: How to use Spring Web Services and REST support in conjunction with JAXB 2 annotations (part 2) – BEKK Open