(Async) SOAP Clients with JDK 11

Application Integration - Part II

This blog post is the second of a series I've just started.

The previous one dealt with some recipes to deal with backend REST APIs integration, using Swagger Codegen, Retrofit and showcasing Portlet's 3.0 PortletAsyncContext in order to manage web service calls in an asynchronous fashion, so as to reduce the footprint of our code on the Thread Pool.

This second blog post now deals with another style of backend services: the SOAP endpoint. Frequent feedback I got from customers is that they had issues migrating from JDK 8 to JDK 11 if they had some SOAP client in their codebase. Indeed, some changes have happened since JDK 9 when some JDK-internal JAXWS and JAXB packages have been removed in an effort to have a more modular and lightweight JVM. Luckily, this is no show stopper and I'll illustrate how I've managed to deal with it. Maybe you've come up with some other solutions of your own. I'd be curious to learn more about it.

And because I dealt with async calls to REST endpoints in the previous blogpost, I also wanted to check whether I could implement the same pattern with CXF SOAP clients and I was happy to find out that it was the case!

As a bonus, I've also covered Liferay's caching framework so as to reduce the amount of request we make to the backend service.

Setting up a SOAP Server

First of all, we need a SOAP server. I've written some Spring Boot dummy SOAP server which is exposing 4 methods:

  • Create a booking (city, departure date, arrival date)
  • List all bookings
  • Check the status of the booking (pending, cancelled, validated)
  • Update the status of the booking 

In order to simulate real-life performance, I've made the backend slow on purpose.

The source code for this project can be found here.

To build it, run:

mvn clean install

To start the server, run:

mvn spring-boot:run

Generating the SOAP Client using cxf-codegen-plugin

CXF is a popular implementation of JAXWS, the Java specification for the Java SOAP web services framework. The maven cxf-codegen-plugin makes it possible to generate the java client code using the WSDL file as an input.

There is one configuration I'd like to point out: the asyncMethods tag inside of wsdlOption. For each method you list here, you'll get additional methods in your client interface which add asynchronous handling capabilities to your SOAP client (see Git repo).

<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>3.3.0</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <sourceRoot>${project.basedir}/target/generated-sources/cxf</sourceRoot>
                <wsdlOptions>
                    <wsdlOption>
                        <wsdl>${project.basedir}/src/main/resources/wsdl/bookings.wsdl</wsdl>
                        <wsdlLocation>classpath:wsdl/bookings.wsdl</wsdlLocation>
                        <bindingFiles>
                            <bindingFile>src/main/resources/bindings/bookings.xml</bindingFile>
                        </bindingFiles>
                        <asyncMethods>
                            <asyncMethod>initBooking</asyncMethod>
                            <asyncMethod>listBookings</asyncMethod>
                            <asyncMethod>updateBookingStatus</asyncMethod>
                            <asyncMethod>checkBookingStatus</asyncMethod>
                        </asyncMethods>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
        </execution>
    </executions>            
</plugin>

In order for those classes to be used outside, you'll have to export their packages in the bnd.bnd file (see Git repo):

Bundle-Name: booking-soap-client
Bundle-SymbolicName: com.liferay.samples.fbo.booking.soap.client

Export-Package: \
    com.liferay.samples.fbo.bookings.model,\
    com.liferay.samples.fbo.bookings_web_service

Usually, once you had generated that code, you used to be ready to fire SOAP requests. This was true until JDK8.

Dealing with JDK 11

The difference with JDK 8 is that you need to provide an implementation of javax.xml.ws.spi.Provider. The Sun implementation was removed from the JDK starting with JDK 9.

I was able to solve this problem through the registration of the CXF implementation of Provider from inside of my Port Factory component (See git repo):

@Component(
    immediate = true,
    service = BookingsPortFactory.class
    )
public class BookingsPortFactory {

    private BookingsPortService bookingsPortService;
    private URL wsdlURL;

    @Activate
    @Modified
    public void activate(BundleContext bundleContext, Map<String, Object> properties) {

        ServiceReference<Provider> providerServiceReference = bundleContext.getServiceReference(Provider.class);
        if(providerServiceReference == null) {
            ProviderImpl providerImpl = new ProviderImpl();
            Dictionary<String, Object> providerProperties = new Hashtable<>();
            bundleContext.registerService(Provider.class, providerImpl, providerProperties);
        }
        
        this.wsdlURL = getClass().getClassLoader().getResource("wsdl/bookings.wsdl");
        this.bookingsPortService = new BookingsPortService(wsdlURL);
    }
    
    public BookingsPort getPort() {
        
        BookingsPort port = bookingsPortService.getBookingsPortSoap11();
        return port;
    }

}

However, this particular ProviderImpl class is not visible from my OSGi bundle by default. As a consequence, I had to write one Fragment Bundle so that the Liferay bundle containing its package exports it (see Git repo).

Bundle-Name: cxf-provider-fragment
Bundle-SymbolicName: com.liferay.sample.fbo.cxf.provider.fragment
Fragment-Host:  com.liferay.portal.remote.soap.extender.impl
Export-Package: org.apache.cxf.jaxws.spi;version=3.2.5

Adding CXF Interceptors

CXF allows you to integrate interceptors both at the client and the server level. Using interceptors, you can add some processing in the SOAP request and response processing. For example, you may add some built-in Logging interceptors in order to write the SOAP messages to the log (see Git repo).

public BookingsPort getPort() {
        
    BookingsPort port = bookingsPortService.getBookingsPortSoap11();
        
    Client cxfClient = (Client) port;
    cxfClient.getInInterceptors().add(new LoggingInInterceptor());
    cxfClient.getOutInterceptors().add(new LoggingOutInterceptor());
        
    return port;

}

As you can see, every port you get from the factory also implements the CXF Client interface, allowing you to inject your interceptors.

Adding some cache management

I could have used the BookingsPort directly from my portlet. Instead, I've added an additional layer responsible for cache management. This way, the BookingsPort is abstracted away behind a BookingLocalService (see Git repo).

public interface BookingLocalService {

    public Booking checkBookingStatus(String bookingId);
    public Booking updateBookingStatus(String bookingId, BookingStatusEnum status);
    public Booking initBookingRequest(BookingInformation bookingInformation);
    public List<String> listBookings(int start, int count);

    Future<?> listBookingsAsync(int start, int count, AsyncHandler<ListBookingsResponse> listBookingsAsyncHandler);

}

There's an additional one that returns a Future<?>. We'll get back to this one later.

The implementation of initBookingRequest uses the BookingsPort (see Git repo):

public Booking initBookingRequest(BookingInformation bookingInformation) {

    InitBookingRequest request = new InitBookingRequest();
    request.setBookingInformation(bookingInformation);

    InitBookingResponse response = _bookingsPort.initBooking(request);
    return response.getBooking();

}

And let's have a look at cache management for the checkBookingStatus method (see Git repo):

@Override
public Booking checkBookingStatus(String bookingId) {

    CheckBookingStatusRequest request = new CheckBookingStatusRequest();
    request.setBookingId(bookingId);

    BookingContentKey key = new BookingContentKey(bookingId);
    BookingCacheItem bookingCacheItem = _portalCache.get(key);
        
    if(bookingCacheItem == null) {

        _log.info("Cache missed for bookingId " + bookingId);

        CheckBookingStatusResponse response = _bookingsPort.checkBookingStatus(request);
        Booking booking = response.getBooking();
        _portalCache.put(key, new BookingCacheItem(booking));
            
        _log.info("Cache entry added for bookingId " + bookingId);

        return booking;
            
    } else {
            
        _log.info("Cache hit for bookingId " + bookingId);
            
        return bookingCacheItem.getBooking();
    }

}

With this class representing the Cache Item (see Git repo):

public class BookingCacheItem implements Serializable {

    private static final long serialVersionUID = 1L;

    private Booking booking;

    public Booking getBooking() {
        return booking;
    }

    public void setBooking(Booking booking) {
        this.booking = booking;
    }

    public BookingCacheItem(Booking booking) {
        super();
        this.booking = booking;
    }
    
}

The idea is to first check the cache and to fallback to a web service call if the item is absent.

However, the status of the booking may change. In my example, I've considered that Liferay is the only place that could trigger a status change. In a real life example, you may have to manage extra cache invalidation scenarios.

Whenever the portlet is going to call the updateBookingStatus method, I'm going to remove the entry from the cache before calling the status update web service:

public Booking updateBookingStatus(String bookingId, BookingStatusEnum status) {

    UpdateBookingStatusRequest request = new UpdateBookingStatusRequest();
    request.setBookingId(bookingId);
    request.setBookingStatus(status);
        
    BookingContentKey key = new BookingContentKey(bookingId);
    _portalCache.remove(key);
        
    _log.info("Cache entry removed for bookingId " + bookingId);
        
    UpdateBookingStatusResponse response = _bookingsPort.updateBookingStatus(request);
        
    return response.getBooking();
}

Integration in an MVC Portlet

This time, I've written a simple MVC Portlet. All the portlet needs to do in order to call the service is to have a @Reference to BookingLocalService (see Git Repo):

@Reference
private BookingLocalService bookingLocalService;

Asynchronous Resource Request

And now, like in the example from the previous blog post, we're taking advantage of Portlet 3.0's support of PortletAsyncContext in the Resource Phase (see Git repo). The idea, again, is to find ways to have less Threads that end up being stuck waiting for some blocking I/O (because our pool of threads is limited).

public void doServeResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse)
        throws PortletException {
        
    int start = ParamUtil.get(resourceRequest, "start", 0);
    int count = ParamUtil.get(resourceRequest, "count", 20);
        
    PortletAsyncContext asyncContext = resourceRequest.startPortletAsync(resourceRequest, resourceResponse);
    ListBookingsAsyncListener listener = new ListBookingsAsyncListener();
    asyncContext.setTimeout(10000);
    asyncContext.addListener(listener);
        
    ListBookingsAsyncHandler handler = new ListBookingsAsyncHandler(asyncContext);
    Future<?> future = _bookingLocalService.listBookingsAsync(start, count, handler);
    handler.setFuture(future);

    listener.setHandler(handler);
        
    asyncContext.start(handler);
        
}

The listBookingsAsync method takes a callback handler as a parameter and returns a future that we're going to use to cancel requests in case our ResourceResponse times out.

The handler reacts to CXF's callback and handles the response (see Git repo):

public void handleResponse(Response<ListBookingsResponse> res) {

    PortletRequestDispatcher portletRequestDispatcher;
    ResourceRequest resourceRequest = this._asyncContext.getResourceRequest();
    ResourceResponse resourceResponse = this._asyncContext.getResourceResponse();
        
    LOG.debug("ListBookingsAsyncHandler handles response");

    try {
        ListBookingsResponse listBookingsResponse = res.get();
        resourceRequest.setAttribute("bookings", listBookingsResponse.getBookingId());
        LOG.debug("ListBookingsAsyncHandler dispatches to /listAsync.jsp");
        portletRequestDispatcher = resourceRequest.getPortletContext().getRequestDispatcher("/listAsync.jsp");
    } catch (InterruptedException | ExecutionException e) {
        LOG.error("ListBookingsAsyncHandler exception", e);
        LOG.debug("ListBookingsAsyncHandler dispatches to /error.jsp");
        portletRequestDispatcher = resourceRequest.getPortletContext().getRequestDispatcher("/error.jsp");
    }
        
    try {
        portletRequestDispatcher.include(resourceRequest, resourceResponse);
        resourceResponse.flushBuffer();
    } catch (PortletException | IOException e) {
        LOG.error("ListBookingsAsyncHandler exception", e);
    }

    this._asyncContext.complete();
        
}

The handler determines whether it is a succesful or exceptional outcome and determines which JSP is going to be used to output the response using a PortletRequestDispatcher.

Eventually, the AsyncContextListener takes care of managing the timeout of the ResourceResponse in order to cancel the CXF web service call in case it's taking too long (see Git repo):

@Override
public void onTimeout(PortletAsyncEvent evt) throws IOException {
    handler.cancelRequest();
}

The handler having a method to cancel the request through the reference to the Future<?> (see Git repo):

public void cancelRequest() {
    _future.cancel(true);
}

Conclusion and next steps

The architecture of this project can be summarized with the following diagram:


See the complete source code of the sample project in this git repository (and follow the instructions in the readme file to deploy the required bundles to your Liferay installation).

As for the previous blog post, this is new practice and I expect that this sample project may start some discussion around the ideas shared in the blog post, leading to some corrections and improvements so as to define future best practices.

Expect some time before the next blog posts in this series. Next thing I'd like to tackle is integration with some message bus technology, using either ActiveMQ, RabbitMQ, Kafka or maybe XMPP (a chat protocol). This could be a good opportunity to explore how to manage server to browser communication, and check what's the current status of WebSockets support for example.