Monday, November 1, 2010

the ease of jax-ws 2.x

Webservices. I've had a long time hate-hate relationship with them. SOAP for example is a good idea, but it just isn't a standard. Try and make a Java client talk with a .NET service - it ain't pretty.

Still I have come to respect a little fellar called jax-ws, which makes it so incredibly easy to open up a class to the brave new world, with only a few annotations (and a server like JBossAS). This here article will describe what you need to do, from the server setup to the maven poms to the actual code.


Patching JBoss for compatible webservices

JBoss comes with its own webservices stack that out of the box just doesn't really cut it in my opinion, although it depends on the version of JBoss of course. But there is one thing that saves it all; the Metro webservices stack. Installing JBossWS Metro basically makes your JBoss be able to provide webservices compatible with most of anything. Note that this will have little to no effect on connecting to webservices; jax-ws does all the work there.

Which version of the metro patch you need, depends on which version of JBossAS you use. I especially recommend installing it on JBoss 4.2.X variants.

JBoss 4.2.3 - metro patch 3.1.1GA.
Jboss 5.1 - metro patch 3.2.0GA.

If you are working with JBossAS 6, you may want to experiment with the stack that comes pre-installed. It may just work for you. In this article I will assume JBoss 5.1.

The metro webservices stack can be installed using Apache ANT. Unzip to a temp folder and rename the file ant.properties.example to ant.properties. Now inside this file, alter the propery jboss510.home to your JBoss 5.1 root directory. Afterwards, run:

ant deploy-jboss510

And voila, the metro patch has been installed!


Jax-ws Maven setup

The server now prepared, we'll want to create a project to do two things:

1) create a webservice
2) create a client to talk to this webservice

So we'll create a project built out of two modules, which I will simply call *server* and *client*.


The parent pom

If you don't mind, I am going to cut these poms a little short. For examples of more complete poms, see my article on the Maven2 pom templates.

We define two modules:

<modules>
    <module>server</module>
    <module>client</module>
  </modules>

Our dependency management for this project is very basic.

<dependencyManagement>
     <dependencies>
       <dependency>
         <groupId>wstestapp</groupId>
         <artifactId>wstestapp-client</artifactId>
         <version>1.0.0-SNAPSHOT</version>
         <type>ejb</type>
      </dependency>
       <dependency>
         <groupId>wstestapp</groupId>
         <artifactId>wstestapp-server</artifactId>
         <version>1.0.0-SNAPSHOT</version>
         <type>ejb</type>
       </dependency>
       <dependency>
         <groupId>javax.javaee</groupId>
         <artifactId>javaee-api</artifactId>
         <version>5</version>
         <scope>provided</scope>
       </dependency>
     </dependencies>
   </dependencyManagement>

We'll add one more later on, but for now this is enough. I added the jee dependency to be able to compile EJBs; if you use JBossAS6 then modify the dependency accordingly (you need javax:javaee-api:6.0). Make sure that the JEE dependency is provided of course. You need the java.net Maven repository for these dependencies to be found.

Notice that there is absolutely no dependency for jax-ws in there; that is because jax-ws is part of Java6, so we already have everything we need to compile jax-ws and JAXB code.

That being said, we need Java6 to compile this code. Configure as follows:

<plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <inherited>true</inherited>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
          <optimize>true</optimize>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>

Again, for a more complete example of plugin management in Maven, see my article on it.

The client and server pom


Again these are really basic and I'm going to show only the interesting part, which are the dependencies. The rest you can figure out for yourself from my Maven article. In both modules, define the following dependency:

<dependencies>
    <dependency>
      <groupId>javax.javaee</groupId>
      <artifactId>javaee-api</artifactId>
    </dependency>
  </dependencies>

Again, we'll add a couple more later. But for now this is all you need.


On disc, the folder structure of your project should be something like this:

projectdir/wstestapp/pom.xml
projectdir/wstestapp/client/pom.xml
projectdir/wstestapp/client/src/main/java
projectdir/wstestapp/client/src/main/resources
projectdir/wstestapp/client/target/classes
projectdir/wstestapp/server/pom.xml
projectdir/wstestapp/server/src/main/java
projectdir/wstestapp/server/src/main/resources

Standard Maven project setup really. Note that I explicitely listed the client target/classes directory; you'll see why later on when we generate the client stub classes.


The webservice EJB


The power of JAX-WS webservices lies in the fact that with only a few simple annotations, you can transform an EJB into a webservice. Lets see that right now. We define a simple hello world EJB:

local interface
@Local
public interface WebServiceTestLocal {

  public String helloWorld();
  public String helloUser(String name);
}

EJB
@Stateless
public class WebServiceTest implements WebServiceTestLocal {
  
  public String helloWorld(){
    return "Hello world!";
  }
  
  public String helloUser(String name){
    return "Hello " + name + "!";
  }
}

This EJB defines two methods: a simpel hello world one and a method that will echo back a name we send to it.

Now let me write the exact same EJB again, but this time turned into a webservice.

local interface
@Local
@WebService()
@SOAPBinding(style = javax.jws.soap.SOAPBinding.Style.DOCUMENT)
public interface WebServiceTestLocal {

  public String helloWorld();
  public String helloUser(String name);
}

EJB
@Stateless
@WebService(endpointInterface = "wstest.WebServiceTestLocal",serviceName="WebServiceTest")
public class WebServiceTest implements WebServiceTestLocal {
  
  public String helloWorld(){
    return "Hello world!";
  }
  
  public String helloUser(@WebParam String name){
    return "Hello " + name + "!";
  }
}

That is it! You have now succesfully created a SOAP webservice out of your EJB! Deploy this as an EJB on JBoss 4.2.3 or 5.1 and when you boot the instance you will notice in the logging that it secretly deployed the webservice as a war (even though it is an EJB), making it available through the web. If everything is correct, you'll get something close to the following logging in your server (Note: JBoss 5.1 logging will be added later):

JBoss 4.2.3 log output
JmxKernelAbstraction] installing MBean: jboss.j2ee:jar=wstest.jar,name=WebServiceTest,service=EJB3 with dependencies:
[EJBContainer] STARTED EJB: wstest.WebServiceTest ejbName: WebServiceTest
EJB3Deployer] Deployed: file:/serverpath/server/wstest/deploy/wstest.jar/
[DefaultEndpointRegistry] register: jboss.ws:context=wstest,endpoint=WebServiceTest
[TomcatDeployer] deploy, ctxPath=/wstest, warUrl=.../tmp/deploy/wstest.jar968294628122087434.war/
[WSDLFilePublisher] WSDL published to: file:/serverpath/server/wstest/data/wsdl/wstest.jar/WebServiceTest811903617484336490.wsdl

Open a browser and navigate to http://localhost:8080/wstest/WebServiceTest?wsdl to see the WSDL that we are going to use to create a client for this baby.

Note on the url:
The url is determined by the name of the jar; in this case the name is 'wstest.jar', so the webservice context is 'wstest'. The serviceName declared in the annotations is the actual name of the webservice that you invoke.

Note on deployment
Don't know what to do at this point? Perhaps my article on hot deployment can help you!

Running the webservice without JBoss


Jax-ws provides us a little tool to make the webservice available without even running Jboss. What you can do is add the following main() method to your webservice EJB:

public static void main(String[] args) {

    Endpoint.publish(
       "http://localhost:8080/wstest/WebServiceTest",
       new WebServiceTest());
  }


Now you can run your EJB as a standalone java application. You know when it works when nothing happens - no output but also no return to the prompt. This means the webservice is listening, and you can connect to it from a client.


Generating the client code


Before we can create a client, we first need to generate some stub code that will allow us to talk to the webservice's exposed methods. This can be very easily done with a tool called wsgen; you'll find that this tool is part of your Java6 JDK. For ease of use, add the bin directory of your JDK to the system PATH variable. Also, make sure that your JBoss server is still running.

Open up a command prompt. I'll be assuming you use an IDE like Eclipse and that you have created a maven module 'client' in it. Navigate to the root directory of your client module. If the directory 'client/target/classes' does not exist yet, create it now.

Now, invoke this command:

wsimport -keep -s src\main\java -d target\classes -p wstest.client http://localhost:8080/wstest/WebServiceTest?wsdl

This will generate not only the class files for the client stub, but also puts the source files in your client project (Maven specific paths - adjust to your own project environment if you don't use Maven). Here is a breakdown of the arguments:

ArgumentDescription
-keepThis will trigger the sourcefiles to be generated
-sThis is the location where to put the source classes
-dThis is the location where to put the generated class files
-pThis is the package of the generated stub classes

And finally the url of the WSDL is provided; this is the reason why your server should still be running. Note that the generation of the class files is quite redundant as your IDE will now create them for you from the source files. But at least now you have seen how to do it.

If your generated code does not compile...
You may be using a Mavenized Eclipse project and the javaee 5 API. At this point you might get compile errors on the generated stub classes. This problem lies in the ordering of the build path and is easily fixed.

Open up the project properties in Eclipse, choose build path and select the order and export tab. Here, make sure that your java runtime is above the maven dependencies in the list; this ensures that the jax-ws of your Java6 runtime is used and not the one that is part of the JEE5 API, which is unfortunately outdated.

We now have the tools we need to invoke the webservice from a client!


Command line client

First lets make a little command line client to test it out. This is the code:

public class HelloWorldTest {
  
  static WebServiceTest service = new WebServiceTest();

  
  public static void main(String[] args){
    
    WebServiceTestLocal port = service.getWebServiceTestPort();
    String test = port.helloWorld();
    System.out.println("WebServiceTest said: " + test);
    
    test = port.helloUser("gimby");
    System.out.println("WebServiceTest said: " + test);
  }
}

TheWebServiceTest and WebServiceTestLocal classes are from the generated stub classes; to make this work, simply make sure the classes are on the classpath when you compile/run this (most likely by adding this test class to the same module). Run the client with either JBoss or the commandline service running and you should get the response back!

the EJB client

Now for the real deal; getting it to work in an EJB environment. It is quite easy actually.

Local interface
@Local
public interface HelloWorldService {

  public String helloWorld();
  public String helloUser(String username);
}

Bean
@Stateless
public class HelloWorldServiceBean implements HelloWorldService {

  @WebServiceRef
  static WebServiceTest service = null;
  
  
  public String helloWorld(){
    WebServiceTestLocal port = service.getWebServiceTestPort();
    return port.helloWorld();
  }
  
  public String helloUser(String username){
    
    WebServiceTestLocal port = service.getWebServiceTestPort();
    return port.helloUser(username);
  }
}


That's it! The @WebServiceRef annotation will make JBoss inject an instance of the webservice for us (of course, make sure the stub classes are in the classpath of your client. In fact, just make sure the classes are IN your client, like I suggested during the wsimport step).


Of course, we cannot just invoke an EJB, we need some kind of interface to it.

Creating a simple test JMX interface

JBoss allows us to setup a neat little JMX-interface. Before we can do that, we need to add some dependencies to our project. This is different for JBoss 4.2.X and Jboss 5.1, I'll list them both:

Jboss 4.2.3
In the parent pom, add these dependencies to the dependency management:

<dependency>
        <groupId>jboss</groupId>
        <artifactId>jboss-annotations-ejb3</artifactId>
        <version>4.2.3.GA</version>
        <scope>provided</scope>
      </dependency>
      <dependency>
        <groupId>jboss</groupId>
        <artifactId>jboss-jmx</artifactId>
        <version>4.2.3.GA</version>
        <scope>provided</scope>
      </dependency>
      <dependency>
        <groupId>jboss</groupId>
        <artifactId>jboss-ejb3</artifactId>
        <version>4.2.3.GA</version>
        <scope>provided</scope>
      </dependency>

And in your client pom, add these dependencies as well:

<dependency>
      <groupId>jboss</groupId>
      <artifactId>jboss-annotations-ejb3</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>jboss</groupId>
      <artifactId>jboss-ejb3</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>jboss</groupId>
      <artifactId>jboss-jmx</artifactId>
      <scope>provided</scope>
    </dependency>


JBoss 5.1
We need only one dependency here. I'll shorten this to the parent pom, you should know what to do in your client pom:

<dependency>
        <groupId>org.jboss.ejb3</groupId>
        <artifactId>jboss-ejb3-ext-api</artifactId>
        <version>1.1.1</version>
        <scope>provided</scope>
        <exclusions>
          <exclusion>
            <groupId>org.jboss.javaee</groupId>
            <artifactId>jboss-ejb-api</artifactId>
          </exclusion>
          <exclusion>
            <groupId>org.jboss.metadata</groupId>
            <artifactId>jboss-metadata</artifactId>
          </exclusion>
        </exclusions>
      </dependency>

The exclusions prevent classpath polution - otherwise you get a big chunk of JBoss in your dependencies AND local maven repository that you really do not need.


Now that we have the needed dependencies, writing the code is simple:

JMX local interface
@Local
public interface JmxHelloWorld {
  
  public String helloWorld();
  public String helloUser(String username);
}

JMX bean
@Service(objectName = "wstest:service=JmxHelloWorld", name = "JmxHelloWorld")
@Management(JmxHelloWorld.class)
public class JmxHelloWorldBean implements JmxHelloWorld {
  
  @EJB
  private HelloWorldService service;
  
  
  public String helloWorld(){
    return service.helloWorld();
  }
  
  public String helloUser(String username){
    
    return service.helloUser(username);
  }
}

So basically the JMX bean becomes a wrapper around our EJB. However, when you deploy this in JBoss, you can then go to the jmx-console. In the list you should find an entry called wstest, with the single JmxHelloWorld entry. Click on it to expose the two EJB methods; invoking them should net you the response of the webservice!


A note on deployment ordering

When you deploy both the webservice and the client EJB module to the same JBoss instance, you might run into deployment ordering problems. What should happen is that the webservice should deploy before the client EJB, as the client EJB depends on it.

You can enforce this in JBoss by deploying the client jar to a subdirectory deploy.last, such that the jar will be:

JBOSS_HOME/server/INSTANCE/deploy/deploy.last/client.jar

Whatever is in the deploy.last directory will be deployed after everything else.

I only add this section just in case you run into problems, Jboss should be able to work out the dependency injection ordering itself. If you need finer grained control, my JBoss 5.1 migration guide contains a section on enforcing specific deployment ordering.

Going deeper: complex types

Until now we've seen the typical "hello world!" type of example that doesn't really cut it if you really want to use this technology. Of course we want to be throwing around our own objects. This is surprisingly easy using Jax-ws. All you do is create your POJO classes and you add a few JAXB annotations here and there.

Lets see an example where a person can be registered through our webservice.

public RegisterResult register(@WebParam Person person){
  
  RegisterResult result = new RegisterResult();
  try{
    // register person here and get the generated ID
    result.setResult(Boolean.TRUE);
    result.setID(the_generated_id);

  } catch(Throwable t){
    // registration failed
    result.setResult(Boolean.FALSE);
    result.setMessage("Registration resulted in an error.");
    return result;
  }

  return result;
}

The Person class is a simple POJO with basic properties. Name, address, etc. As long as you use basic types, you don't have to do anything special. However: use object types, not primitive types. And provide a no-arg constructor. These objects will be marshalled/unmarhalled by the JAXB API, so if you want to know more about what it can do I suggest you read up on it.


public class Person {
  private String name;
  private String address;
  private String zipcode;
  private String city;
  private Boolean bald;

  public Person(){
  }

  ...
}

The interesting class is the RegisterResult. This object gives some feedback to the callee.

@XmlRootElement
public class RegisterResult {
  private Boolean result;
  private String message;
  private String ID;

  ...
}

This object allows for some basic communication back to the client. Note the XmlRootElement annotation; this is a JAXB annotation that marks this object as being something that JAXB can marshall/unmarshall. Usually you don't even need this annotation, but it is good practice to put it there anyway. When you want to return a List of objects for example, all objects in the list need to be annotated with @XmlRootElement. Plus the annotation adds documentation to your code stating it will be used by webservice calls.

The ID can be used by the client to later do more operations on the registered person. For example, the client may want to retrieve the information back.

public FetchResult getPerson(@WebParam String ID){
  // fetch person based on the ID here.
  Person person = findPersonByID(ID);

  FetchResult result = new FetchResult();
  result.setPerson(person);
  result.setResult(Boolean.TRUE);

  return result;
}

You see I like returning result objects. Your mileage may vary. But lets see this FetchResult object.

@XmlRootElement
public class FetchResult {

  private Boolean result;
  private Person person;
  
  public FetchResult(){
  }

  public Boolean getResult() {
    return result;
  }

  public void setResult(Boolean result) {
    this.result = result;
  }

  @XmlElement(name="person")
  public Person getPerson() {
    return person;
  }

  public void setPerson(Person person) {
    this.person = person;
  }
}

FetchResult is a little more interesting because it contains a nested complex type object; our Person. Using the @XmlElement annotation we can add our Person object as a complex type into the SOAP response. It will end up in a person subelement.

Conclusion

In this article I gave you a few basic tools when working with jax-ws webservices;

- how to apply the metro patch to JBossAS for a more compatible webservice stack
- how to turn an EJB into a webservice
- how to run the webservice from the commandline
- how to run the webservice in JBoss itself
- how to generate client stub classes from the webservice
- how to create a commandline test client
- how to create an EJB consumer client
- how to create a JBoss JMX interface to test the EJB consumer client

Hopefully powered with this knowledge, webservices from this day forward will no longer be a chore.

No comments:

Post a Comment