ctrl-alt-Development
Your hotkey to alternative software development
Building a web service with CXF
What do you need for a web service? In a perfect world this would be a contract or a service definition and an implementation. Most commonly the service definition comes in the form of a WSDL (Web Service Description Language) from which you generate Java code. However, if you don't exactly know yet how the service will work is easier to define the contract as a Java interface and use that to generate the WSDL.
The question is now, what service are we going to make? Well I have a terrible memory for birthdays so I could use a service that reminds me of those. Ok, we'll make a Birthday Calendar service. How would such a service look like? Well probably something like this:
package nl.cad.cxf.testservice;
import java.util.Date;
/** A simple birthday calendar service. */
public interface BirthdayCalendar {
/**
* Holds a single birthday.
*/
public interface Birthday {
String getName();
int getDayOfMonth();
}
/**
* Adds a birthday.
* @param name the name of the person.
* @param date the birthday.
*/
void addBirthday(String name, Date date);
/**
* returns all the birthdays in one month.
* @param month the month (1..12).
* @return all the birthdays in that month.
*/
Birthday[] getBirthdaysInMonth(int month);
}
The service contract has two possible actions: adding a birthday and retrieving all birthdays within a specific month. All you need to make a birthday calendar :) Also this interface contains a number of interesting things that go beyond the usual Hello World web service. To be exact: a complex data type, a date time and a collection of objects. These are the things that often work a little different.
Implementing the interface is predictable:
package nl.cad.cxf.testservice;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
public class BirthdayCalendarImpl implements BirthdayCalendar {
public static class BirthdayImpl implements Birthday {
private String name;
private int dayOfMonth;
private int month;
public BirthdayImpl(String name,Date birthDay) {
this.name=name;
Calendar c=Calendar.getInstance();
c.setTime(birthDay);
dayOfMonth=c.get(Calendar.DAY_OF_MONTH);
month=c.get(Calendar.MONTH)+1; // 1..12
}
public int getMonth() {
return month;
}
public int getDayOfMonth() {
return dayOfMonth;
}
public String getName() {
return name;
}
}
private List<BirthdayImpl> birthdays=new ArrayList<BirthdayImpl>();
public synchronized void addBirthday(String name, Date date) {
birthdays.add(new BirthdayImpl(name,date));
}
public synchronized Birthday[] getBirthdaysInMonth(int month) {
List<Birthday> results=new ArrayList<Birthday>();
BirthdayImpl[] bs=birthdays.toArray(new BirthdayImpl[birthdays.size()]);
for (BirthdayImpl b:bs) {
if (b.getMonth()==month) results.add(b);
}
return results.toArray(new Birthday[results.size()]);
}
}
First we implement the class BirthdayImpl which implements the interface Birthday. Beside the getters which are specified in the service contract we also add a getter for the month and a constructor that takes the right fields from the given name and date. The implementation of BirthdayCalendarImpl becomes easy, adding a date is now just a matter of creating a new instance of BirthdayImpl and adding it to the collection of birthdays. Querying the birthdays within a specific month is done by filtering them.
How to turn this service into a web service.
Ideally speaking this should be everything you need to make a web service: a service contract and a implementation. All other code is bloat. Fortunately there is not much to do. We will transform this service using CXF into a standalone web service. We'll do this based on a unit test so that we can easily test the service from Maven. Maven keeps unit tests separate from the rest of the code. The default location is ./src/test/java/. But the easiest is probably re-using the unit test the artifact plugin generated for you.
package nl.cad.cxf.testservice;
import junit.framework.TestCase;
import org.apache.cxf.frontend.ServerFactoryBean;
import org.apache.cxf.aegis.databinding.AegisDatabinding
public class ServiceTest extends TestCase {
public void testService() throws Exception {
//
ServerFactoryBean svrFactory = new ServerFactoryBean();
svrFactory.getServiceFactory().setDataBinding(new AegisDatabinding());
svrFactory.setServiceClass(BirthdayCalendar.class);
svrFactory.setAddress("http://localhost:8081/Birthday");
svrFactory.setServiceBean(new BirthdayCalendarImpl());
svrFactory.create();
//
Thread.sleep(30000); // A little delay to allow manual testing.
}
}
The ServiceFactyory bean is a CXF API class to easily configure and start a web-service. The data binding tells CFX how to translate Java Objects into XML and vice versa. The service property contains the interface that holds the service contract. The address is the URL on which the service must be made available and the ServiceBean is the implementation of the service. After calling create an embedded web-container will be started (Jetty) on which the service is published.
Configuring Maven for compiling and running the project.
If you try to compile the project with maven (mvn compile) you will be confronted with compiler errors. Maven's default is to compile to source level 1.3 which means that Java5 for loops are not supported. Fortunately its easy to reconfigure Maven to use a different source and target level. Paste the following bit of configuration above the dependencies tag in the pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
Its now possible to test the web service by giving the command 'mvn test'. When the sure-fire test plug-in is running you can open your browser and examine the generated WSDL of the Birthday Service: http://localhost:8081/Birthday?wsdl Looks great! However generated WSDL is not yet a working service. We'll have to make a web service client.
Creating a matching Web Service client.
Creating a web service client with CXF is even easier that publishing one. The class ClientProxyFactoryBean makes it possible to transform a Java interface into a proxy based client. A Proxy is a special class in the java.lang.reflect package which allows the creation of objects that implement a certain interface without having a real implementation (only an implementation of InvocationHandler is needed). CXF translates calls on the interface to a matching web service invocation in this way.
Anyway, the creation of a client is done in the same way as the service. By adding the following method to the ServiceTest class we can create a client:
protected BirthdayCalendar newBirthdayClient() {
ClientProxyFactoryBean factory = new ClientProxyFactoryBean();
factory.setServiceClass(BirthdayCalendar.class);
factory.getServiceFactory().setDataBinding(new AegisDatabinding());
factory.setAddress("http://localhost:8081/Birthday");
return (BirthdayCalendar) factory.create();
}
If we want to test the client using this unit test the web service needs to be available. By moving the web service code to a static block (remove the Thread.sleep) it will be automatically started when JUnit loads the test:
static {
ServerFactoryBean svrFactory = new ServerFactoryBean();
svrFactory.getServiceFactory().setDataBinding(new AegisDatabinding());
svrFactory.setServiceClass(BirthdayCalendar.class);
svrFactory.setAddress("http://localhost:8081/Birthday");
svrFactory.setServiceBean(new BirthdayCalendarImpl());
svrFactory.create();
}
Now we can add a nice test method that will verify if the service does what it should do:
public void testService() throws ParseException {
BirthdayCalendar bc=newBirthdayClient();
SimpleDateFormat sdf=new SimpleDateFormat("dd-MM-yyyy");
bc.addBirthday("Erik",sdf.parse("21-02-1971"));
Birthday[] b=bc.getBirthdaysInMonth(2);
assertEquals(1,b.length);
assertEquals("Erik",b[0].getName());
assertEquals(21,b[0].getDayOfMonth());
}
The test method add a birthday and then checks if this has been really added to the calendar.
By executing 'mvn test' you'll be (hopefully :) greeted with a passed test and a succesful build. If you'd like to follow the communication between the client and the server you can use a utility such as ngrep or wireshark. Keep in mind that all communication runs over the loopback interface (lo).
Running the web service in a web container.
Ok, so now we have a working web service that is standalone. Useful for testing purposes but in the real world you'd want a web-service which is part of a bigger system or running aside other services. In such a case a web-container is more practical.
We'll now transform this project so that it becomes possible to run it as a web-application in Tomcat. The first step is the changing of the packaging tag in the pom.xml. This is now set to 'jar' - a normal standalone jar-file - but for a web-application you will need a war file. Change the packaging tag to 'war' and execute a 'mvn clean install'.
If all is well, maven will now complain about a missing web.xml file. Which is right, as all web-applications need a web.xml. Create in your project an empty file ./src/main/webapp/WEB-INF/web.xml and try again. Now it works. If you look in the target folder, you will see a completely assembled war file together with all its dependencies.
Now we need to fill out the web.xml so that it will make available the web-service when we deploy the war file (now, nothing will happen..) To do this we need two things: we need to declare a servlet that will deal with the web-service requests and we need to configure the web-service it self. The second part we do using the spring-framework.
<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/beans.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>CXFServlet</servlet-name>
<display-name>CXF Servlet</display-name>
<servlet-class>
org.apache.cxf.transport.servlet.CXFServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>CXFServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
The upper part of the web.xml defines the so-called context listener which will get a notification when we web-application is started or stopped. Ideal for configuration purposes. In this case we're using a Spring Context Listener that loads the configuration file beans.xml and initializes the beans defined in it. The second part declares the CXF Servlet that will handle the requests.
The 'beans.xml' file does the actual configuration and makes the web service accessible using the servlet.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:simple="http://cxf.apache.org/simple"
xmlns:soap="http://cxf.apache.org/bindings/soap"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
http://cxf.apache.org/simple http://cxf.apache.org/schemas/simple.xsd">
<import resource="classpath:META-INF/cxf/cxf.xml"/>
<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
<import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
<simple:server
id="birthdayCalendarService"
serviceClass="nl.cad.cxf.testservice.BirthdayCalendar"
address="/birthday">
<simple:serviceBean>
<bean class="nl.cad.cxf.testservice.BirthdayCalendarImpl" />
</simple:serviceBean>
<simple:dataBinding>
<bean class="org.apache.cxf.aegis.databinding.AegisDatabinding"/>
</simple:dataBinding>
</simple:server>
</beans>
This file can be placed in the same folder as the web.xml. First some parts of CXF are initialized (in older versions, this was done automatically) and after that the service is registered. Interestingly enough you need to specify exact the same information as when using the standalone version. Its just another way of saying the same. You also could do this in code but it would have been more work :)
Now that the configuration is done its just a matter of doing another 'mvn clean install' and placing the resulting war file into the webapps directory of Tomcat. If all is well, the servlet will now available at http://localhost:8080/CXFTestService-1.0-SNAPSHOT/. The default behavior of the servlet is to show a list of available services. By clicking on the link of the BirthdayCalendarPort you can examine the WSDL of the service.
To check if the service is really working you can make another unit test (integration test actually :) that calls the deployed service.
package nl.cad.cxf.testservice;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import junit.framework.TestCase;
import nl.cad.cxf.testservice.BirthdayCalendar.Birthday;
import org.apache.cxf.aegis.databinding.AegisDatabinding;
import org.apache.cxf.frontend.ClientProxyFactoryBean;
public class IntegrationTest extends TestCase {
protected BirthdayCalendar newBirthdayClient() {
ClientProxyFactoryBean factory = new ClientProxyFactoryBean();
factory.setServiceClass(BirthdayCalendar.class);
factory.getServiceFactory().setDataBinding(new AegisDatabinding());
factory.setAddress("http://localhost:8080/CXFTestService-1.0-SNAPSHOT/birthday");
return (BirthdayCalendar) factory.create();
}
public void testService() throws ParseException {
BirthdayCalendar bc=newBirthdayClient();
SimpleDateFormat sdf=new SimpleDateFormat("dd-MM-yyyy");
if (bc.getBirthdaysInMonth(2).length==0) {
bc.addBirthday("Erik",sdf.parse("21-02-1971"));
}
Birthday[] b=bc.getBirthdaysInMonth(2);
assertEquals(1,b.length);
assertEquals("Erik",b[0].getName());
assertEquals(21,b[0].getDayOfMonth());
}
}
For the client creation, only the URL changed. The test-code is a little different because you can't tell in advance if a birthday is already present (Tomcat won't be restarted each time :). By executing 'mvn test' again this test will be executed as well. Let's hope it will be successful ;) By the way, normally you'll place Integration tests into a different project because if the service isn't deployed you can't build the project (chicken and the egg). If the test doesn't work, usually you'll have a configuration problem, recheck the XML.
Where are we now?
We now have made a deployed and working web service for which its really easy to change the interface. This is ideal for rapid prototyping, but ultimately you'll want use an industry standard. In the second part of this tutorial we'll transform the Birthday Calendar into a standard JAX-WS and JAXB based web service. With CXF of course!