Oct 21, 2014

JAX-WS client cookie manager - Customized cookie management

Customized cookie management with JAX-WS client  (JAX-WS RI - Metro specific)

For tl;dr option, please GOTO the bottom of this page.

Once upon a time I had to connect to a stateful SOAP webservice. It was stateful, because it cached some user-specific information in the session context, and sent back a session cookie. Bad design or not, this is what I had to work with. Nevertheless, the results of the calls depended on the userId of the caller made from the client application. Thus in the client application for each user, a session had to be maintained for the remote service. This means, the client stub was not exchangeable amongst users.
The inhabitants of internet already know the answer to how to maintain the session:
((BindingProvider)proxy).getRequestContext()  
     .put(BindingProvider.SESSION_MAINTAIN_PROPERTY, true);
It is a very convenient approach, simple as slicing bread.

Plot twist
With above approach, session can be maintened per stub. But as I mentioned, we need a session per user of client application, so we need a stub per session. It was around 160 sessions open when we started getting OutOfMemoryError exceptions. We created a heap dump, and it turns out that one client stub takes up ~2.6Mbytes of memory. Yes the SOAP service interface was quite fat. So we quickly came to the conclusion that having one stub per user is a no go. We either need a pool of stubs with a LRU (Least Recently Used) or LFU (Least Frequently Used) cleanup algorithm to keep the size of stubs managable, or plug in our own session manager into JAX-WS.

One stub to rule them all
To use only one stub in a concurrent environment, calls to the client proxy must be thread safe. Thread safety of client proxies are not guaranteed by the JAX-WS specification[1]. But browsing the source code of the reference implementation[2] (that is also used by Glassfish) reveals that in fact client proxies are thread safe. It's inner datastructures created when issuing a request ensure that no request-specific information is stored in the stub itself, but passed around in the aforementoned data structures.

Finding a slot to stick my cookie jar
I could not find any feasible solution on the land of Internet, how to set the  cookie jarfor the ws stub. I wanted to tell it: use this object to store and retrieve cookies, so I can provide my own policy. This requirement is obvious when you use an Apache HttpClient and define your own CookieStore[3]. There is no way I cannot tell this to a mature and roboust SOAP framework. After digging the source of JAX-WS RI for several hours, I found a solution (for version 2.1). Then I had to face the cold truth: The Glassfish version we use (3.1.3) uses JAX-WS version 2.2.5, where the whole cookie management mechanism was rewritten fortunately, but is still proprietary. So after another couple of hours code shoveling the solution was clear.

HttpConfigFeature


Yes, those blessed guys who made JAX-WS RI hid a java.net.CookieStore into their prorietary HttpConfigFeature[4] class. This class implements WebServiceFeature (introduced in jax-ws 2.1). I may sound cynical here, but I am actually glad they have this feature. It is pretty obvious from the code that they designed a mechanism dedicated to plug in any CookieStore. I am just a little bit sad that it is literally nowhere mentioned in the documetation.[5]
I hereby present you the power of rtfc!
Using HttpConfigFeature
Below you find how to set your own CookieStore when you use JAX-WS RI (Metro) version 2.2.5 (and maybe above?). As I mentoned earlier, when a user logged in to the client application, he had his own session for the webservice too. We used CDI's Session Context to hold the cookies for every user, it was quite convenient.


import javax.xml.ws.Service;
 
protected hu.pal.ws.MyService createStub() {
    Service service = getService();
    MyCookieManager cookieJar = new MyCookieManager();
    WebServiceFeature httpConfigFeature = new HttpConfigFeature(cookieJar);
 
    return getServicePort(service, hu.pal.ws.MyService, "getMyServicePort", httpConfigFeature);
}
 
protected  T getServicePort(Service service, Class serviceInterface,
    String method, WebServiceFeature... features) {
 
    Class serviceStubClazz = service.getClass();
 
    // get endpoint and target namespace from
    // wsimport generated service class annotations
    String webEndpoint = getWebEndpoint(serviceStubClazz, method);
    String targetNamespace = getTargetNamespace(serviceStubClazz);
 
    QName qName = new QName(targetNamespace, webEndpoint);
 
    T stub = service.getPort(qName, serviceInterface, features);
    return stub;
}

References:
[1] - JAX-WS 2.1 Specification (JSR224) - https://jcp.org/aboutJava/communityprocess/mrel/jsr224/index2.html
[2] - JAX-WS RI 2.2.5 Download page - https://jax-ws.java.net/2.2.5/
[3] - Apache HttpClient CookieStore - https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/CookieStore.html
[4] - Metro HttpConfigFeature on GitHub - https://github.com/gf-metro/jaxws/blob/master/jaxws-ri/rt/src/main/java/com/sun/xml/ws/developer/HttpConfigFeature.java
[5] - JAX-WS RI documentation - https://jax-ws.java.net/2.2.8/docs/

1 comment :