Thursday, March 27, 2008

Web Services in Java (Part 1)


Web Services in Java (Part 1)




When learning Web Services in Java, you have two complicating factors:

1) Web Services in Java are built on a tall stack of Java technologies and Web specifications: JAXB, JAX-RPC, JAXM, JAXP, SAAJ, WSDL, UUDI, etc. And to gain a detailed understanding of Web Services you need some understanding of all of these technologies. That's a steep learning curve!

2) There are multiple Web Service implementations, toolkits, and IDE plugins for in Java. All of them work differently to some extent, and all of them are in some degree of flux as these technologies and toolkits evolve and mature. Even when two separate Web Service toolkits adhere to the same standards, they may differ greatly in their implementation.

These two factors present a significant barrier to developers learning how to create robust and secure Web Serivces. And this situation is further complicated by lots of poor and outdated documentation on the subject. Most books and online tutorials I have read take the approach of teaching all the details of all of the myriad technologies (JAXB, JAXM, SAAJ, JAX-RPC, etc.) before you even begin to construct your first "Hello World" Web Service. This may be a convenient way for the author to package her or his knowledge, but it's not a very effective way to learn. I find that many developers learn fastest by jumping right in and getting something working immediately, even if they don't understand how it works, and then expand their knowledge from there in any direction they want.

I suspect that authors tend to focus so much on the Java API's and Web technologies because those are the main elements of Web Services that are universal across all implementations. If they go down the path of creating a complete Web Service by example, they would have to cover different ways of doing a Web Service with each different toolkits, IDE plugins, and Application Servers; and anything they wrote would be outdated in a year or less. So, it's often easier to just present the core technologies and hope the developer can figure out the rest. (ha)

For this article, I'm going to demonstrate creating a Web Service using the Java Web Service Developer Pack 2.0 from Sun, and deploying the Web Serivice to JBoss 4.0. So if you are using Apache Axis, this example probably won't work for you exactly as-is. Sorry. That's the nature of this beast. I plan writing a future article on how to create an Apache Axis Web Service.

The good news is that it doesn't take very much to get a simple Web Service working. The modern toolkits have become easier to use, and do most of the nasty detail work for you. Once you get a simple Web Service working, you can expand to a reasonable set of production-grade functionality without tackling all the learning curves at once. Then, over time, you can choose to drill down into individual advanced aspects of Web Services as the need arises.

For this example, there are at least 4 different paths I could take to start constructing this Web Service, depending on various design decisions and considerations. (WSDL-centric vs. Java-centric, EJB vs. Servlet, etc) But I'm not going to get into all of that discussion. I'm just going to charge ahead on one simple path, and save all the alternatives and philosophy for another article.

Here we go!

Java Web Services : Step 1 - Create a new Web Application

The Web Service for this example is going to be deployed as a regular WAR file. So in your IDE of choice, create a standard Web Application directory structure including source (src) and WebRoot/WEB-INF directories.

There are no Web Service JAR files needed for your WEB-INF/lib directory. Your application server should already have Web Service support libraries installed. In our case, JBoss 4.0 comes with Web Service support.


Java Web Services : Step 2 - Create a Java Interface for your Web Service

This interface will represent the functionality you want to expose with this Web Service.

You can name your interface anything you want. I recommend naming it <Domain>Server where "<Domain>" is any name that represents the services offered. Like "WeatherServer" or "UserServer" or "OrderServer", etc.

This interface must extend the java.rmi.Remote interface. And each method in your interface must throw java.rmi.RemoteException.

Your interface can declare any regular Java methods. The method parameters and method return types can be of any primitive type (int, float, long, etc.), or standard type (String, Long, Calendar, etc.) or an array of objects. You can also use any custom Java Objects of your own. However, there are some limits and design considerations you'll discover when using your own objects. For now, try and keep it simple.

Here is my sample interface:


package com.example.webservice.user;

import java.rmi.Remote;
import java.rmi.RemoteException;
import com.example.webservice.exception.GeneralException;

public interface AccountManagementServer extends Remote{

public boolean isUserKeyValid(String pid)
throws RemoteException, NoSuchAccountException, GeneralException;

public PortableUser getUser(String userName)
throws RemoteException, NoSuchUserException, GeneralException;
}






These methods throw some custom exceptions I have created.

Here is the code for one of those exceptions:


package com.example.webservice.exception;
public class GeneralException extends Exception {
public GeneralException(String message) { super(message); }
public String getMessage() { return super.getMessage(); }
}

There's nothing difficult or special in creating an Exception that can be thrown across a Web Service, but one minor rule you have to follow is that there must be a getter method for any parameter used in a constructor. So, since this constructor takes a "message" as a parameter, we must declare a getMessage() method in this class. (The parent class "Exception" has a getMessage method, but that doesn't count.)


Java Web Service : Step 3 - Create the Implementation


Create a Java class that implements the Web Service interface. Also create any support classes that you want to use.

This class must implement javax.xml.rpc.server.ServiceLifecycle, and implement the interface you defined previously.

You can name this implementation class anything you want, but I recommend just appending "Impl" to the Interface Name. Like "WeatherServerImpl" or "OrderServerImpl".


package com.example.webservice.user;

import java.rmi.RemoteException;
import javax.xml.rpc.ServiceException;
import javax.xml.rpc.server.ServiceLifecycle;
import javax.xml.rpc.server.ServletEndpointContext;
import com.example.webservice.exception.GeneralException;
import com.example.webservice.exception.NoSuchAccountException;
import com.example.webservice.exception.NoSuchUserException;

public class AccountManagementServerImpl implements
AccountManagementServer, ServiceLifecycle {

private ServletEndpointContext ctx;

public boolean isUserKeyValid(String pid) throws NoSuchAccountException,
GeneralException
{
try {
return UserService.isUserKeyValid(pid);
} catch (NoSuchAccountException nsae) {
throw nsae;
} catch (Exception ex) {
throw new GeneralException(ex.toString());
}
}

public PortableUser getUser(String userName)
throws RemoteException, NoSuchUserException, GeneralException {

User user = null;
try {
user = UserService.getUser(userName);
} catch (NoSuchUserException nsue) {
throw nsue;
} catch (Exception ex) {
throw new GeneralException(ex.toString());
}
if (user == null) return null;

//Convert the big User object down to the small PortableUser object
PortableUser pu = new PortableUser();
pu.setUserName(user.getUserName());
pu.setPassword(user.getPassword());
pu.setPortalId(user.getPortalId());
return pu;
}

public void init(Object context) throws ServiceException {
ctx = (ServletEndpointContext) context;
}

public void destroy() {
}
}

The code above uses another application service called "UserService". The source code is not provided here, but it is not important. The key concept is that your Web Service implementation should re-use an pre-existing application services whenever possible. Keep as much business logic out of your Web Service as possible.

In the above example, one of the methods in my Web Service returns an object of type "PortableUser". This is just a simple data class that I created as follows:


package com.example.webservice.user;

public class PortableUser {
private String userName;
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName;}

private String portalId;
public String getPortalId() { return portalId; }
public void setPortalId(String portalId) { this.portalId = portalId;}

private String password;
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password;}
}

I created the object "PortableUser", because my application's regular "User" object is a huge object with a large number of methods and nested child objects. If I had used my User object as a return value for my Web Service, performance would have suffered significantly.

Besides, I only wanted to expose a subset of my User object anyway. In cases like these, you should make lightweight Java classes (Data Transfer Objects) that package the values you want to use in your Web Service.


Java Web Service : Step 4 - Create a configuration file

We are now going to use a Web Service generator tool to create the Web Service WSDL and JAX-RPC mapping files. If you don't know what WSDL and JAX-RPC is, don't worry about it. The WSDL file describes your Web Service to the outside world, and the JAX-RPC mapping files tie the WSDL into your Java Classes.

The Web Service generator tools can generate WSDL files from Java classes, OR they can generate Java classes from WSDL files. In our case, we started by defining a Java class (an interface), and so we will be generating the WSDL. But there are times when you might want to design a Web Service starting with a hand-made WSDL file. But that's another design discussion to be addressed in a future article.

In Apache Axis, the two relevant tools are called (appropriately) "WSDL2Java" and "Java2WSDL".

In the JWSDP, there is only one tool, called "wscompile", and it can perform either conversion. I'm going to use wscompile for this example.

For wscompile, you will need to create a config file for generating your Web Service. This config file is only used to generate the WSDL and mapping files. You can name the config file anything you want, but I recommend you name it config-<Domain>Service.xml. Example: "config-UserService.xml". If you have more than one Web Service package to generate, you will need to have more than one config file, which is why I recommend against naming it just "config.xml".

There are 5 things you must define in your config file. Two of these are namespaces, and there really are a lot of different ways you could name your namespaces, but I'll leave that for a future discussion. What I offer below are just loose suggestions we can use for now:



config-AccountManagementService.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
<service name="AccountManagementService"
targetNamespace="http://com.example.webservice.user/"
typeNamespace="http://com.example.webservice.user/types"
packageName="com.example.webservice.user">
<interface name="com.example.webservice.user.AccountManagementServer"/>
</service>
</configuration>

Notes about the above configuration file:

  • The Web Service name is "AccountManagementService".

  • The "targetNamespace" defines the namespace to use for the Web Service.*

  • The "typeNamespace" defines the namespace for your Web Services types. I simply added "types" to the end of my targetNamespace.

  • The packageName is the name of the Java package where your Web Service classes live, and where any generated classes are to be placed. (We will not be generating classes in this example.)

  • The interface name is the full name of your Java Interface for the Web Service.


*A namespace is just a universally unique string. It is not a URL, even though it's a common practice to format namespaces as URL's. It's also common practice to include Java package name information in the namespace name. The format I've chosen here does both....and I'm starting to think that's confusing.

Another way to name a namespace is to specify a real http domain, and then use the URL path to try and specify the remaining parts of your package path that aren't referenced in the domain (in reverse order). If your website is "myapp.example.com", and your package name is com.example.myapp.webservice.user, then your namespace could be "http://myapp.example.com/webservice/user". This is less confusing, but not representative of the true package name.

The source of the confusion here is that Java packages are named in descending order (com.example.myapp) but URL's are always named in ascending order (myapp.example.com). And if you make any effort to mix the two things, you'll have problems. =)

So for my example Web Service here, don't take my namespace naming method too seriously.


Java Web Service : Step 5 - Generate the WSDL file and the JAX-RPC mapping files


wscompile.sh is part of the Java WebServices Developer Pack from Sun. And in this example, I'm going to show using wscompile from the command line, and using wscompile from an ant task. You can choose to run wscompile either way. The command line version might get you going faster, but for long-term manageability I recommend eventually creating the ant task.

The Java Web Services Development Pack (with wscompile) can be downloaded from here: http://java.sun.com/webservices/jwsdp/index.jsp

You could also use Java2WSDL from Apache Axis. Both will generate somewhat similar results, assuming both tools have done a good job of adhering to the same versions of the same standards.

Once you have the JWSDP installed, you can run wscompile.sh from the command-line as follows:

wscompile.sh -cp WebRoot/WEB-INF/classes -verbose -define -f:rpcliteral
-nd WebRoot/WEB-INF/wsdl
-mapping WebRoot/WEB-INF/mapping-AccountManagementService.xml
config-AccountManagementService.xml

You can use "wscompile.sh -help" to get detailed information on what all these parameters do.

For automated building and less command-line fussing, I recommend using an ant task for wscompile. To run the wscompile ant task, you need the following JAR files in your ant's classpath. All of these JAR files can be found in the JWSDP:

activation.jar
jaas.jar
jaxrpc-api.jar
jaxrpc-impl.jar
jaxrpc-spi.jar
jta-spec1_0_1.jar
mail.jar
relaxngDatatype.jar
resolver.jar
saaj-api.jar
saaj-impl.jar
xmlsec.jar
xsdlib.jar

Once these libraries are in your ant's path, the following task will generate the WSDL and mapping files you need:

<taskdef name="wscompile" classname="com.sun.xml.rpc.tools.ant.Wscompile" />
<target name="generate">
<wscompile verbose="true" classpath="WebRoot/WEB-INF/classes"
define="true" features="rpcliteral" nonclassdir="WebRoot/WEB-INF/wsdl"
mapping="WebRoot/WEB-INF/mapping-AccountManagementService.xml"
config="config-AccountManagementService.xml"/>
</target>

Notes about the above ant task:

  • "classpath" is the path of your interface and related classes, as well as any external jars your code needs.

  • "define" indicates that we are generating just the WSDL and mapping files, and no other classes.

  • "features" are the various options you can use for specifying what type of WSDL to generate.

  • "mapping" is the location of the output JAX-RPC mapping files.

  • "nonclassdir" is the location to place the WSDL file (and other nonclass files that are generated)

  • "config" is the name of the config file for wscompile



Whether you use the ant file, or the command-line, you must execute this wscompile from the project's root directory, because that's how I've setup this example. (That's why you see "WebRoot/WEB-INF" prepended to the wsdl and mapping locations.) You could also run wscompile from the WebRoot/WEB-INF directory itself, and then you wouldn't need those directories specified in all your parameters above.



Java Web Service : Step 6 - Add your service to your web.xml file

A Web Service can either be created as a Web Component (essentially a servlet), or an EJB. In this example, I have created this Web Service to be a Web Component, and so it must be defined in web.xml as a servlet. The servlet-class is just the Web Service implementation class I've already created.

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns=http://java.sun.com/xml/ns/j2ee
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">

<servlet>
<servlet-name>AccountManagementService</servlet-name>
<servlet-class>com.example.webservice.user.AccountManagementServerImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>AccountManagementService</servlet-name>
<url-pattern>/AccountManagementService</url-pattern>
</servlet-mapping>
</web-app>



Java Web Service : Step 7 - Add your service to your webservices.xml file

If you don't have a webservices.xml file, you can just add one to your WEB-INF directory. This is the file that will connect all the pieces together, and specifies which WSDL file goes with which mapping file and which servlet.

Add service to webservices.xml


<?xml version="1.0" encoding="UTF-8"?>

<webservices
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:impl="http://com.example.webservice.user/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://java.sun.com/xml/ns/j2ee
http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd"
version="1.1">

<webservice-description>
<webservice-description-name>AccountManagementService</webservice-description-name>
<wsdl-file>WEB-INF/wsdl/AccountManagementService.wsdl</wsdl-file>
<jaxrpc-mapping-file>
WEB-INF/mapping-AccountManagementService.xml
</jaxrpc-mapping-file>
<port-component>
<port-component-name>PortComponent</port-component-name>
<wsdl-port>impl:AccountManagementServerPort</wsdl-port>
<service-endpoint-interface>
com.example.webservice.user.AccountManagementServer
</service-endpoint-interface>
<service-impl-bean>
<servlet-link>AccountManagementService</servlet-link>
</service-impl-bean>
</port-component>
</webservice-description>

</webservices>

Here are the key parts of this file you have to get right:

  1. The xmlns:impl (near the top) must be the same as the namespace you used in your config file for generating the WSDL.

  2. The wsdl-file specifies the location of your WSDL file relative from WebRoot

  3. The jaxrpc-mapping-file specifies your mapping file, also relative from WebRoot.

  4. The wsdl-port will be "impl:" + <your interface class name> + "Port". When you learn more, you'll learn what a "Port" is, and see how the Port name is defined in the WSDL file, and how it is referenced by this "impl" namespace.

  5. The service-endpoint-interface is the full name of your interface class.

  6. The servlet-link is the "servlet-name" you defined in your web.xml



Java Web Service : Step 8 - Package and deploy these files

There's nothing special about building a Web Service war file. Just package up all if the class files you have created, plus the generated XML and WSDL files. A simple ant-build for the war should suffice:



<target name="build-war">
<war destfile="wsexample.war" webxml="WebRoot/WEB-INF/web.xml">
<classes dir="WebRoot/WEB-INF/classes" includes="**/*"/>
<lib dir="WebRoot/WEB-INF/lib" includes="*.jar"/>
<webinf dir="WebRoot/WEB-INF" includes="**/*.xml,**/*.wsdl"
excludes="web.xml"/>
</war>
</target>


Deploy your WAR file to your application server (in my case JBoss). If all is well, you should see messages in the log that indicate your Web Service has self-installed gracefully. (Note: When using JBoss, you must have the jboss-ws4ee.sar module installed).

Java Web Service : Step 9 - Verify the Web Service is installed

You can test that your Web Service is installed using the following URL:

http://localhost/wsexample/AccountManagementService?wsdl

When you call your Web Service with the "?wsdl" parameter, it should respond by giving you it's WSDL.

Alternatively, you can go to http://localhost/ws4ee. You'll get two links that will verify that Web Services are running properly on your server, and another link that will show that your Web Service is installed.

These tests will only ensure that your Web Serivice has a proper WSDL file. To really test the full functionality of your Web Service, you need to create some kind of test client that can call your services. You can either write a simple client of your own (covered in the next step), or you can use some of the various IDE plugin's on the market (like the Web Services Explorer for Eclipse) that will self-discover the details of your service.


Java Web Service : Step 10 - Test the Web Service

Making a Web Service client is just a miniature version of the previous steps you went through to make a Web Service itself.

You start off by making any kind of Java project you want. It doesn't have to be a Web Application.

This time we'll be generating client classes instead of WSDL. Create your configuration file for wscompile as follows:

config-AccountManagementServer.xml

<configuration
xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
<wsdl location="http://localhost/wsexample/AccountManagementService?wsdl"
packageName="com.example.webservice.user.client"/>
</configuration>

Specify the URL of your Web Service (with ?wsdl appended) in the "location" attribute. Specify the package name of the classes to generate in the "packageName" attribute.

Next, run wscompile as follows:

wscompile.sh -verbose -gen:client -f:rpcliteral -s src -d src -keep
-mapping mapping-AccountManagementServer.xml
config-AccountManagementServer.xml

OR, if using ant:

<taskdef name="wscompile" classname="com.sun.xml.rpc.tools.ant.Wscompile" />
<target name="generate">
<wscompile verbose="true" client="true" features="rpcliteral" sourceBase="src"
base="src" keep="true" mapping="mapping-AccountManagementService.xml"
config="config-AccountManagementService.xml"/>
</target>

Finally, here is a Test Class you can run that calls the Web Service methods:


package com.example.webservice.user.test;

import java.net.URL;
import javax.xml.namespace.QName;
import javax.xml.rpc.Service;
import javax.xml.rpc.ServiceFactory;
import com.example.webservice.user.client.AccountManagementServer;
import com.example.webservice.user.client.PortableUser;

public class ServiceTest {

public static final String host = "http://localhost";
public static final String path = "/wsexample";

public static void main(String args[]) {
try {
URL url = new URL(host + path + "/AccountManagementService?wsdl");
QName qname = new QName("http://com.example.webservice.user/",
"AccountManagementService");
ServiceFactory factory = ServiceFactory.newInstance();
Service service = factory.createService(url, qname);
AccountManagementServer userServer = (AccountManagementServer)
service.getPort(AccountManagementServer.class);
PortableUser user = userServer.getUser("demo");
System.out.println(user.getUserName());
System.out.println(user.getPortalId());
System.out.println(user.getPassword());
} catch (Exception e) {
System.out.println("ServiceTest: " + e);
}
}
}

And that concludes our end-to-end Web Service example. =)





1 comments:

mahakk01 said...

Web services in Java is very interesting topic. There are two complication for web services in java which are metioned in this post. At first look one can find the steps and code difficult but actaully they are not. Just go through them.
digital certificate