Thursday, December 18, 2008

Invoke a single port async service using JAX-WS and WS-Addressing

I was looking over the WSTF pages and found the use cases that include a single port reply to another address. I did wonder if you could safely invoke this using JAX-WS. So I decided to find out.

To make things simple I used the following hello world with addressing turned on. Note this is not a true asynchronous service as the rely is sent before the server returns with a 202 code; but it is close enough for the purposes of this presentation.

@Addressing
@WebService
public class Hello {

    @Action(input="http://project1/Hello/sayHello"
            output="http://project1/Hello/sayHelloReponse")
    public String sayHello(String name) {
        return name;
    }
}

So I published this service and then generated a client to another project. We need to create a suitable callback service so first of all you have to find the port SEI:

@WebService(wsdlLocation="HelloService.wsdl", targetNamespace="http://project1/",
  name="Hello")
@XmlSeeAlso(
  { project1.ObjectFactory.class })
public interface Hello
{
  @WebMethod
  @Action(input="http://project1/Hello/sayHelloRequest", output="http://project1/Hello/sayHelloResponse")
  @ResponseWrapper(localName="sayHelloResponse", targetNamespace="http://project1/",
    className="project1.SayHelloResponse")
  @RequestWrapper(localName="sayHello", targetNamespace="http://project1/",
    className="project1.SayHello")
  @WebResult(targetNamespace="")
  public String sayHello(@WebParam(targetNamespace="", name="arg0")
    String arg0);
}

Then I used Refactor->Duplicate to create a copy and converted the interface into a class. As you can see you need to turn the interface in-side out. The changes I made are:

  • Remove WSDL declaration in @WebService, a new one will be create for response part
  • Remove output action and replace with input with the output value
  • Make method one way
  • Add @Addressing modifier
  • Make the return value the only parameter, moving over any WebResult values to WebParam.

If would be really nice to have this done automatically when the usingAddressing tag is found in the WSDL, perhaps it would be possible to extend the web services generation tool to do this.

@WebService(targetNamespace = "http://project1/", name = "Hello")
@XmlSeeAlso( { ObjectFactory.class })
@Addressing
public class HelloCallback {
    @WebMethod
    @Action(input = "http://project1/Hello/sayHelloResponse")
    @RequestWrapper(localName = "sayHelloResponse",
                     targetNamespace = "http://project1/",
                     className = "project1.SayHelloResponse")
    @Oneway
    public void sayHello(@WebParam(targetNamespace = "", name = "return")
        String ret) {
        
        System.out.println(ret);
        
    }
}

And then the code is really quite simple, in this case we publish the endpoint and then invoke the service. Note we use the @OneWayFeature to populate the required WS-Addressing features, this is a bit naughty as it is part of the -RI and doesn't support faults. For the purposes of this blog though it makes the code a mite easier to read.

The other thing to note is that the call to sayHello returns a "null" object, you can use the MessageContext to check the response code if you want to confirm a 202; but otherwise the rest of the action is deferred to the callback class at this point.

public static void main(String[] args) {
    helloService = new HelloService();
    
    
    Endpoint e = Endpoint.publish(
        "http://localhost:7890/endpoint",
        new HelloCallback());
    

    WSEndpointReference replyTo = new WSEndpointReference(e.getEndpointReference());


    Hello hello = helloService.getHelloPort(new WebServiceFeature[] {
      new OneWayFeature(true, replyTo)});


    // This method will return null
    //
    
    Object ret = hello.sayHello("Bob");

    // Check the message response code
    //

    Integer responseCode = (Integer)     
        ((BindingProvider)hello).getReponseContext()
            .get(MessageContext.HTTP_RESPONSE_CODE);  
}

One final note is that you need to force the client to exit as Endpoint.publish create a non-daemon thread.

10 comments:

J said...

Thanks so much Gerard. Your posts are the only ones I can find online teaching how to use JAX-WS and WS-Addressing to implement asynchronous invocations.

I am just wondering if there is a alternative annotation in JAX-WS spec of "@OneWayFeature". The "@AddressingFeature" could not take a replyTo Endpoint Reference.

Thanks again,

Justin

Gerard Davison said...

Sure,

No problem, glad to be of service.

No alternative to OneWayFeature I am afraid. Probably worth raising a bug over at jax-ws.dev.java.net. You can write you own though as JAX-WS RI is really quite extensible.

Might just not be an area they care about directly yet as glassfish as it only just starting to get deep into SOA where async services become more prevalent.

Gerard

Unknown said...

Thanks for trying to explain this - I've been struggling for a few days to find an example of how to create a simple async service with jax-ws.
Unfortunately, I've not been able to get the example to work. When I send the message to the server, the following stack trace is output to the console and the method implementation is never called.


javax.xml.ws.WebServiceException: java.io.IOException: Stream is closed
at com.sun.xml.internal.ws.streaming.TidyXMLStreamReader.close(TidyXMLStreamReader.java:58)
at com.sun.xml.internal.ws.server.sei.EndpointArgumentsBuilder$DocLit.readRequest(EndpointArgumentsBuilder.java:524)
at com.sun.xml.internal.ws.server.sei.EndpointMethodHandler.invoke(EndpointMethodHandler.java:232)
at com.sun.xml.internal.ws.server.sei.SEIInvokerTube.processRequest(SEIInvokerTube.java:82)
at com.sun.xml.internal.ws.api.pipe.Fiber.__doRun(Fiber.java:587)
at com.sun.xml.internal.ws.api.pipe.Fiber._doRun(Fiber.java:546)
at com.sun.xml.internal.ws.api.pipe.Fiber.doRun(Fiber.java:531)
at com.sun.xml.internal.ws.api.pipe.Fiber.runSync(Fiber.java:428)
at com.sun.xml.internal.ws.server.WSEndpointImpl$2.process(WSEndpointImpl.java:232)
at com.sun.xml.internal.ws.transport.http.HttpAdapter$HttpToolkit.handle(HttpAdapter.java:460)
at com.sun.xml.internal.ws.transport.http.HttpAdapter.handle(HttpAdapter.java:233)
at com.sun.xml.internal.ws.transport.http.server.WSHttpHandler.handleExchange(WSHttpHandler.java:95)
at com.sun.xml.internal.ws.transport.http.server.WSHttpHandler.handle(WSHttpHandler.java:80)
at com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:65)
at sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:65)
at com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:68)
at sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:555)
at com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:65)
at sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:527)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.io.IOException: Stream is closed
at sun.net.httpserver.LeftOverInputStream.read(LeftOverInputStream.java:67)
at java.io.FilterInputStream.read(FilterInputStream.java:66)
at com.sun.xml.internal.ws.transport.http.server.ServerConnectionImpl$1.close(ServerConnectionImpl.java:131)
at com.sun.xml.internal.ws.streaming.TidyXMLStreamReader.close(TidyXMLStreamReader.java:56)
... 21 more


Also, the stubs generated from the wsdl (jax-ws 2.1.7) do not contain @Action annotations (although the wsdl does contain wsaw:UsingAddressing and an action for the input)

Any ideas what I'm doing wrong? Or any chance that you could post full sources?

Gerard Davison said...

Mark H,

I am not sure, unfortunately I don't have the original code having been a bit to vigorous in tidying up my home directory.

If you have a simple example project you can send me in a .zip, you will need to alter the file extension to get past filters, to gerard.davison@oracle.com and I will take a look.

Gerard

Unknown said...

Thank you very much for the tutorial. I have a problem running it
though. I deployed the service and it can be easily used the standard
way. However when I want to utilize the callback, the main method runs
forever and doesn't return anything. When I stop it manually, Netbeans
comes up with the message: BUILD STOPPED.

Any advice?

Thanks is advance,
Tom

Gerard Davison said...

Tom

the java vm won't stop until you do a System.exit() and you have started a server. The trick is to do this after you have finished in the callback; but not during.

Gerard

Aswin said...

Thanks for this article Gerad. It was so much useful while using CXF for this purpose. I have a post that shows the difference here.

Aswin said...

Thanks a lot of the article. It was really useful and I wrote a post that deals with the same situation , but using Apache CXF as the provider and is here

jasonray said...

Thanks a bunch. I have been struggling with this for a few days now, and am happy to see a good example. It seems like this is a need that could be handled by the stack, and the fact that it isn't almost makes me think that few are utilizing this feature in WS-Addressing.

Thanks again!

Hema said...

Thanks Gerard for writing such helpful post.

I was just wondering how shall i be able to verify that response comes to HelloCallBack and the required code is executed.

Your response will be really helpful.

Thanks,
Hema