Web Service programming with SPRING-WS, JAXB2, and JAX-WS


Server-side

Message Dispatching

Spring-WS XML message dispatching resembles Spring MVC, you define endpoint mapping, MessageDispatcher finds the endpoint by message content, then it searchs for the EndpointAdapter by asking each registered adapter if it supports the endpoint; then it uses the adapter found to invoke the endpoint. We’ll come back to this later.

Integrate Spring-WS with Spring MVC

As described in my previous blog Spring MVC HandlerMapping, HandlerAdapter, and Webflow MVC Integration, we can do the similar thing for Spring-WS.

  1. We use SimpleUrlHandlerMapping for URL to handler mapping (you can define multiple SimpleUrlHandlerMapping and they will be searched by order). The handler would be a WsdlDefinition or the aforementioned MessageDispatcher.
  2. We define a WsdlDefinitionHandlerAdapter for WsdlDefinition;
  3. And we define a WebServiceMessageReceiverHandlerAdapter for MessageDispatcher.
    A MessageDispatcher is a WebServiceMessageReceiver and WebServiceMessageReceiverHandlerAdapter supports WebServiceMessageReceiver.
    This is how a handler is linked to adapter;
  4. All HandlerAdapters will be auto detected (see previous blog).
  5. We need define a SaajSoapMessageFactory, which is required by WebServiceMessageReceiverHandlerAdapter. (The other factory
    available is AxiomSoapMessageFactory, which uses StAX and performs better for large messages)
    <bean class="org.springframework.ws.transport.http.WebServiceMessageReceiverHandlerAdapter">
        <property name="messageFactory" ref="messageFactory"/>
    </bean>

    <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory">
        <property name="soapVersion">
            <util:constant static-field="org.springframework.ws.soap.SoapVersion.SOAP_11"/>
        </property>
    </bean>

    <bean class="org.springframework.ws.transport.http.WsdlDefinitionHandlerAdapter">
        <property name="transformLocations" value="true" />
    </bean>

    <bean id="wsUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/services/**">messageDispatcher</prop>
                <prop key="holiday.wsdl">holiday</prop>
            </props>
        </property>
        <property name="order" value="200" />
    </bean>

    <bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher">
    </bean>

    <sws:dynamic-wsdl id="holiday" portTypeName="HumanResource" locationUri="/holidayService/"
                      targetNamespace="http://mycompany.com/hr/definitions">
        <sws:xsd location="/WEB-INF/xsd/hr.xsd"/>
    </sws:dynamic-wsdl>

Note that we use namespace based wsdl definition. It equals to define a DefaultWsdl11Definition bean (with SimpleXsdSchema).
If you have multiple schemas in the wsdl and want to inline them (recursively), you only need to put Apache WS-Commons XML Schema in
classpath for namespace based wsdl configuration. Or you can explicitly define one like so:

    <bean id="b1" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition">
        <property name="schemaCollection" ref="schemaCollection"/>
        ...
    </bean>
    <bean id="schemaCollection" class="org.springframework.xml.xsd.commons.CommonsXsdSchemaCollection">
        <property name="xsds">
            <list>
                <value>/WEB-INF/xsd/b11.xsd</value>
                <value>/WEB-INF/xsd/b12.xsd</value>
            </list>
        </property>
        <property name="inline" value="true"/>
    </bean>

If you use MessageDispatcherServlet, then you don’t have to define handlers and adapters because the servlet already defined default handlers and adapters. You don’t need to define handler-mapping either – the default is to map “.wsdl” to a wsdl bean with that id; and map the rest to MessageDispatcher.

Message Dispatching and processing

Now we come back to Message Dispatching. Once we integrated with MVC, SOAP request can be dispatched to Spring-WS MessageDispatcher. We need to process the XML message. The configuration can be simplified if we use Spring-WS namespace based configuration. All you need is to make sure “component-scan” scans org.springframework.ws.server.endpoint.annotation.Endpoint and define

<sws:annotation-driven/>

This will enables PayloadRootAnnotationMethodEndpointMapping and SoapActionAnnotationMethodEndpointMapping. It also enables DefaultMethodEndpointAdapter, which registers payload processors for dom4j, jaxb2, jdom, stax, etc. As said in the 1st section, the adapter will invoke the the ws endpoint.

After this, if you see IllegalStateException: No adapter for endpoint: Is your endpoint annotated with @Endpoint..., it’s most likely not a configuration problem. Check your handler method signature – for instance, if using JAXBElement, make sure the parameter is annotated with @RequestPayload. (If using older GenericMarshallingMethodEndpointAdapter, then the @RequestPayload is not required on parameter type JAXBElement; It’s required by DefaultMethodEndpointAdapter, which is registered by <sws:annotation-driven/>; for other parameter types, check the requirement of the corresponding processor & adapter).

JAXB2 paramters and return type

For JAXB2 processor, if JAXB2 is in classpath, it is XmlRootElementPayloadMethodProcessor, which subclass AbstractJaxb2PayloadMethodProcessor. It will unmarshal XML request payload to JAXB2 classes.

As you can see, it will create a new JAXB marshaller (v.s. Spring marshaller) for every operation, which is as suggested.

JAXBContext v.s. JAXB marshaller cost and thread-safety; v.s. Spring-WS JAXB marshaller

JAXBContext is thread safe and should only be created once and reused to avoid the cost
of initializing the metadata multiple times. Marshaller and Unmarshaller are not thread
safe, but are lightweight to create and could be created per operation.

See also http://jaxb.java.net/guide/Performance_and_thread_safety.html and http://stackoverflow.com/questions/7400422/jaxb-creating-context-and-marshallers-cost

Spring Jaxb2Marshaller is an extra layer over JAXB marshaller. Each Spring marshaller contains a singleton JAXBContext. Spring creates a JAXB marshaller for each marshalling operation as suggested by JAXB (this involves setting JAXB properties, listeners, event handlers, adapters, and schema). (on server-side, you normally don’t use Spring Jaxb2Marshaller directly unless need MTOM. Instead XmlRootElementPayloadMethodProcessor calls JAXB directly.)

Endpoint handling method (handler)

You use annotation to define the endpoint and handler.

Can I use classes generated by xjc directly as domain object?

No. Those classes is primarily for xml binding. Not only because it’s cluttered with JAXB
annotations, but also that some types are xml-specific. For instance, xsd “date” type is
bound to “javax.xml.datatype.XMLGregorianCalendar”.

Handling SOAP Message with Excel and Image attachment with JAXB
<xs:element name="imageReport" type="xs:base64Binary" xmime:expectedContentTypes="image/jpeg"/>
<xs:element name="excelReport" type="xs:base64Binary" xmime:expectedContentTypes="application/vnd.ms-excel"/>

Java activation DataHandler
If you don’t specify xmime:expectedContentTypes=”application/vnd.ms-excel” for instance, then the mapped java type will be byte[]. DataHandler is more convenient.

What is “xmime:expectedContentTypes”?
See Describing Media Content of Binary Data in XML at http://www.w3.org/TR/xml-media-types/.

How to load/save an awt Image? How to convert an image to different format (e.g. jpg to gif)?
Use ImageIO.

xmime:expectedContentType to Java type mapping:

=========                    =========
MIME Type                    Java Type
=========                    =========
image/*                      java.awt.Image
text/plain                   java.lang.String
application/xml, text/xml    javax.xml.transform.Source
(others)                     javax.activation.DataHandler

xmime:contentType v.s. xmime:expectedContentType:

The schema can further use the xmime:contentType attribute to designate the actual content type of the binary data used in the message. (In contrast, xmime:expectedContentTypes specifies what are allowed. This combination allows you to say “image/* is expected but this message contains image/jpeg”.)

Sample Code

HolidayRequest is generated by xjc, which is annotated with @XmlRootElement and thus will be processed by XmlRootElementPayloadMethodProcessor and JAXB will be used to unmarshal.

@Endpoint
public class HolidayEndpoint {
    private ObjectFactory objectFactory = new ObjectFactory();

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "HolidayRequest")
    @ResponsePayload
    public EmployeeHolidayReportResponse handleHolidayRequest(@RequestPayload HolidayRequest holidayRequest) throws Exception {
        Date startDate = SchemaConversionUtils.toDateTime(holidayRequest.getHoliday().getStartDate()).toDate();
        Date endDate = SchemaConversionUtils.toDateTime(holidayRequest.getHoliday().getEndDate()).toDate();
        String name = holidayRequest.getEmployee().getFirstName() + ' ' + holidayRequest.getEmployee().getLastName();
        humanResourceService.bookHoliday(startDate, endDate, name);

        EmployeeHolidayReportResponse response = objectFactory.createEmployeeHolidayReportResponse();
        response.setEmployee(holidayRequest.getEmployee());
        response.setYear(SchemaConversionUtils.toXMLGregorianCalendar(new DateTime()));

        /*
         * Image attachment
         */
        BufferedImage image = ImageIO.read(new FileInputStream(SpringWebUtils.getWebAppRootDir() + "/WEB-INF/misc/ws/employee-holiday-report.jpg"));
        response.setImageReport(image);

        /*
         * Excel attachment
         */
        FileDataSource fileDataSource = new FileDataSource(SpringWebUtils.getWebAppRootDir() + "/WEB-INF/misc/ws/employee-holiday-report.xlsx");
        DataHandler excelFileDataHandler = new DataHandler(fileDataSource);
        response.setExcelReport(excelFileDataHandler);

        return response;
    }
}

Build with xjc

If you have multiple top-level schema files and each has different package, then you need to configure multiple execution for jaxb2-maven-plugin.

By default, code is generated under “target/generated-sources/jaxb/”. The generated folder is auto added as a maven source folder.

Default package name

If no -p “pkg” parameter to xjc, it will use the spec’d algorithm to calculate
a default pkg name. See JAXB spec section D5. “Generating a Java package name”.

For instance, targetNamespace “http://www.acme.com/go/espeak.xsd&#8221; will be mapped
to pkg “com.acme.go.espeak” (‘www’ is always ignored).

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxb2-maven-plugin</artifactId>
    <version>1.3.1</version>
    <executions>
        <execution>
            <id>exec-xjc-1</id>
            <goals>
                <goal>xjc</goal>
            </goals>
            <configuration>
                <schemaDirectory>src/main/webapp/WEB-INF/xsd</schemaDirectory>
                <schemaFiles>hr.xsd</schemaFiles>
                <packageName>com.ema.onlinestore.ws.schema.samples.hr</packageName>
                <outputDirectory>${project.build.directory}/generated-sources/jaxb/hr</outputDirectory>
                <staleFile>${project.build.directory}/jaxb2/.xjcStaleFlag1</staleFile>
            </configuration>
        </execution>
        <execution>
            <id>exec-xjc-2</id>
            <goals>
                <goal>xjc</goal>
            </goals>
            <configuration>
                <schemaDirectory>src/main/webapp/WEB-INF/xsd/samples</schemaDirectory>
                <schemaFiles>messages.xsd</schemaFiles>
                <packageName>com.ema.onlinestore.ws.schema.samples.airline</packageName>
                <outputDirectory>${project.build.directory}/generated-sources/jaxb/airline</outputDirectory>
                <staleFile>${project.build.directory}/jaxb2/.xjcStaleFlag2</staleFile>
            </configuration>
        </execution>
    </executions>
</plugin>

Logging

On the debug level, only the payload root element is logged; on the TRACE level, the entire message content.

log4j.logger.org.springframework.ws.server.MessageTracing=TRACE
log4j.logger.org.springframework.ws.client.MessageTracing.sent=TRACE
log4j.logger.org.springframework.ws.client.MessageTracing.received=DEBUG

Client-side

Spring-WS client

Spring-WS client uses Spring WebServiceTemplate and JAXB classes for marshalling to XML. Hence we only need to use xjc to generate the JAXB binding classes as on server-side (v.s. JAX-WS client needs to use wsimport to generate service classes too).

JAXBContext is thread safe and should only be created once and reused to avoid the cost
of initializing the metadata multiple times. Marshaller and Unmarshaller are not thread
safe, but are lightweight to create and could be created per operation.

Spring marshaller is an extra layer over JAXB marshaller. Each Spring marshaller contains
a singleton JAXBContext. You usually define one per client. However if your client needs to
access different web services, you could define one per URI (defaultUri).

To use jaxb2-maven-plugin to generate classes from wsdl, you need to set
<xmlschema>false</xmlschema> besides <wsdl>true</wsdl>.

Note: Even though the generated files between server and client are identical,
you should not share them. Here we actually put them in different package.
(as in real world, client code is a typically from a different company)

<bean id="client" abstract="true">
    <property name="defaultUri" value="http://localhost:8080/onlinestore/services"/>
</bean>

<!--
 | If your client is not extending WebServiceGatewaySupport, just inject a bean of
 | class WebServiceTemplate with the identical configuration properties.
 -->
<bean id="saajClientHr" class="com.ema.onlinestore.ws.client.springws.hr.SaajClient" parent="client">
    <constructor-arg>
        <ref bean="messageFactory"/>
    </constructor-arg>
    <property name="marshaller" ref="marshaller"/>
    <property name="unmarshaller" ref="marshaller"/>
</bean>

<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory">
    <property name="soapVersion">
        <util:constant static-field="org.springframework.ws.soap.SoapVersion.SOAP_11"/>
    </property>
</bean>

<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <!--
     | 1. Must set either "contextPath" or "classesToBeBound".
     | 2. Multiple context path is seperated by ":" (as supported by JAXB)
     | 3. There's helper property "contextPaths" that accepts a list of String (so your xml looks neater)
     |
     | This matches the -p <pkg> of xjc.
     |
    <property name="contextPath" value="com.mycompany.pkg1:com.mycompany.pkg2"/>
     -->
    <property name="contextPaths">
        <list>
            <value>com.ema.onlinestore.ws.schema.samples.client.hr</value>
        </list>
    </property>
</bean>

public class SaajClient extends WebServiceGatewaySupport {
    private ObjectFactory objectFactory = new ObjectFactory();
    private void holidayRequest() throws DatatypeConfigurationException {
        HolidayRequest request = objectFactory.createHolidayRequest();
        EmployeeType employee = objectFactory.createEmployeeType();
        HolidayType holiday = objectFactory.createHolidayType();
        request.setEmployee(employee);
        request.setHoliday(holiday);

        employee.setFirstName("Fei");
        employee.setLastName("Ma");
        employee.setNumber(BigInteger.valueOf(100));
        DateTime startDate = new DateTime();
        DateTime endDate = startDate.plusDays(2);
        holiday.setStartDate(SchemaConversionUtils.toXMLGregorianCalendar(startDate));
        holiday.setEndDate(SchemaConversionUtils.toXMLGregorianCalendar(endDate));

        EmployeeHolidayReportResponse response = (EmployeeHolidayReportResponse)
            getWebServiceTemplate().marshalSendAndReceive(request);

        /*
         * Image attachment
         */
        File jpgOutput = new File("./target/employeeHolidayReport.jpg");
        try {
            ImageIO.write(toBufferedImage(response.getImageReport()), "jpg", jpgOutput);
        } catch (IOException ioex) {
            System.err.format("Can't write to file %s", jpgOutput.getAbsolutePath());
        }
        File gifOutput = new File("./target/employeeHolidayReport.gif");
        try {
            ImageIO.write(toBufferedImage(response.getImageReport()), "gif", gifOutput);
        } catch (IOException ioex) {
            System.err.format("Can't write to file %s", gifOutput.getAbsolutePath());
        }

        /*
         * Excel attachment
         */
        File reportFile = new File("./target/employeeHolidayReport.xlsx");
        FileOutputStream fos = null;
        DataHandler dataHandler = response.getExcelReport();
        try {
            fos = new FileOutputStream(reportFile);
            dataHandler.writeTo(fos);
        } catch (IOException ioex) {
            System.err.format("Can't write to file %s", reportFile.getAbsolutePath());
        } finally {
            IOUtils.closeQuietly(fos);
        }
        System.out.format("Received holiday report of type [%s] for %s of year %d. %nThe report is saved at %s%n%n",
                dataHandler.getContentType(),
                response.getEmployee().getFirstName() + ' ' + response.getEmployee().getLastName(),
                response.getYear().getYear(),
                reportFile.getAbsolutePath());
    }
}

<artifactId>jaxb2-maven-plugin</artifactId>
<schemaFiles>holiday.wsdl</schemaFiles>
<xmlschema>false</xmlschema>
<wsdl>true</wsdl>
<packageName>com.ema.onlinestore.ws.schema.samples.client.hr</packageName>

 

JAX-WS client

JAX-WS client uses wsimport generated service classes to access the endpoint. The generated service class get endpoint address from wsdl. You can override it programmingly as shown in the example.

<bean id="jaxwsClientHr" class="com.ema.onlinestore.ws.client.jaxws.hr.JaxWsClient">
    <property name="endpointAddress" value="http://localhost:8080/onlinestore/services"/>
</bean>

public class JaxWsClient {
    private ObjectFactory objectFactory = new ObjectFactory();
    private String endpointAddress;

    private void holidayRequest() throws DatatypeConfigurationException {
        HumanResourceService service = new HumanResourceService();

        HumanResource endpoint = service.getHumanResourceSoap11();
        BindingProvider provider = (BindingProvider) endpoint;

        String serviceLocation = (String) provider.getRequestContext().get(BindingProvider.ENDPOINT_ADDRESS_PROPERTY);
        serviceLocation = this.endpointAddress;
        provider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serviceLocation);

        HolidayRequest request = objectFactory.createHolidayRequest();
        ... fill in request object (same as Spring-WS client code)

        EmployeeHolidayReportResponse response = endpoint.employeeHolidayReport(request);

        ... process response object (same as Spring-WS client code)
}

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxws-maven-plugin</artifactId>
    <version>1.10</version>
    <executions>
        <execution>
            <goals>
                <goal>wsimport</goal>
            </goals>
            <configuration>
                <wsdlUrls>
                    <!-- <wsdlUrl>http://localhost:8080/onlinestore/holiday.wsdl</wsdlUrl> -->
                    <wsdlUrl>file:///${project.basedir}/../spring-ws/src/main/resources/samples/hr/holiday.wsdl</wsdlUrl>
                </wsdlUrls>
                <packageName>com.ema.onlinestore.ws.schema.samples.client.hr</packageName>
            </configuration>
        </execution>
    </executions>
</plugin>

Schema Design Guidelines

This link http://www.xfront.com/GlobalVersusLocal.html
explains the following XML schema design:

  • Russian Doll Design
  • Salami Slice Design
  • Venetian Blind Design

In most situations, we should choose Venetian Blind Design.

Hide/Expose Namespace

See http://www.xfront.com/HideVersusExpose.html

With a good design, you could switching it on and off using elementFormDefault=”unqualified”/”qualified”.

There are two requirements on an element for its namespace to be hidden from instance documents:

  • The value of elementFormDefault must be “unqualified”.
  • The element must not be globally declared.

A component (element, complexType, or simpleType) is “global” if it is an immediate child of <schema>.

Anonymous XML type v.s. named type and their impact to JAXB and Spring-WS

See http://www.w3.org/TR/xmlschema-0/#InlineTypDefn.

Whether you choose anonymous type or named type changes the JAXB mapping and Spring-WS endpoint handling method parameter type.

  • For anonymous, the global element “HolidayRequest” is directly mapped to java class
    HolidayRequest, which is annotated with @XmlRootElement;
  • For named, the global element “HolidayRequest” is NOT mapped to any java class,
    instead ObjectFactory contains an extra method to create it:public JAXBElement<HolidayRequestType> createHolidayRequest(HolidayRequestType value)
  • Named type is mapped to a java class (annotated with @XmlType(name=”HolidayRequestType”)); but
    anonymous type can NOT be mapped to a java class (only its containing element).

Because XxxType is not an XML element, thus it can’t be used as endpoint handling method input parameter or return type (as required by JAXB; and as a result, Spring XmlRootElementPayloadMethodProcessor requires the object must be annotated with @XmlRootElement or is a JAXBElement too). Instead, you need use JAXBElement<XxxType>, which represents the XML element of that type.

Server:

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "HolidayRequest")
    @ResponsePayload
    public JAXBElement<EmployeeHolidayReportResponseType> handleHolidayRequest(@RequestPayload JAXBElement<HolidayRequestType> holidayRequest){
        HolidayRequestType holidayRequestType = holidayRequest.getValue();
        ...
        JAXBElement<EmployeeHolidayReportResponseType> response = objectFactory.createEmployeeHolidayReportResponse(responseType);
        return response;
    }

Client:

    HolidayRequestType requestType = objectFactory.createHolidayRequestType();
    JAXBElement<HolidayRequestType> request = objectFactory.createHolidayRequest(requestType);
    ...
    JAXBElement<EmployeeHolidayReportResponseType> response = (JAXBElement<EmployeeHolidayReportResponseType>)
        getWebServiceTemplate().marshalSendAndReceive(request);
    EmployeeHolidayReportResponseType responseType = response.getValue();
Advertisements
This entry was posted in Computers and Internet, J2EE, Java, JAX-WS, JAXB, Springframework, Web Service. Bookmark the permalink.

2 Responses to Web Service programming with SPRING-WS, JAXB2, and JAX-WS

  1. Pingback: Spring WS template « TechnoBuzz

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s