Tuesday, January 07, 2014

Using CXF as a JAX-RS server and client

I recently implemented a JAX-RS service and client using CXF and I think it's brilliant. Why? Because here's a unit test for my service code:
@Test
public void testGetCustomerById() {
    int id = random.nextInt();
        
    Customer expected = new Customer(id);
    when(customerDao.getById(id)).thenReturn(expected);
        
    Customer actual = customerService.getCustomerById(id);
    assertEquals(expected, actual);
}
And here's an integration test using the client:
@Test
public void testGetCustomerById() {
    int id = random.nextInt();
        
    Customer expected = new Customer(id);
    when(customerDao.getById(id)).thenReturn(expected);
        
    Customer actual = customerService.getCustomerById(id);
    assertEquals(expected, actual);
}
Yup, they're the same test! This makes me happy.

So, how is it done? We'll start with the service interface.
@Path("/customer")
public interface CustomerService {
    
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    Customer getCustomerById(@PathParam("id") int id);
}
And an implementation that satisfies the unit test above.
public class CustomerServiceImpl implements CustomerService {
    
    @Inject CustomerDao customerDao;

    @Override
    public Customer getCustomerById(int id) {
        return customerDao.getById(id);
    }
}
Wiring up the unit test is simple, so here's what the integration test looks like in full:
public class CustomerServiceClientTest {
    
    // This is the client
    private CustomerService customerService;

    // CXF JAX-RS server
    private Server server;

    private Random random = new Random();

    @Before
    public void before() {
        CustomerServiceImpl serviceImpl = new CustomerServiceImpl();
        serviceImpl.customerDao = mock(CustomerDao.class);

        JAXRSServerFactoryBean serverFactory = new JAXRSServerFactoryBean();
        serverFactory.setAddress("http://localhost:9090");
        serverFactory.setProvider(new JacksonJsonProvider());
        serverFactory.setServiceBean(serviceImpl);
        server = serverFactory.create();

        JAXRSClientFactoryBean clientFactory = new JAXRSClientFactoryBean();
        clientFactory.setAddress("http://localhost:9090");
        clientFactory.setProvider(new JacksonJsonProvider());
        clientFactory.setServiceClass(CustomerService.class);
        customerService = clientFactory.create();
    }

    @After
    public void after() {
        service.destroy();
    }

    @Test
    public void testGetCustomerById() {
        int id = random.nextInt();
        
        Customer expected = new Customer(id);
        when(customerDao.getById(id)).thenReturn(expected);
        
        Customer actual = customerService.getCustomerById(id);
        assertEquals(expected, actual);
    }
}
The key thing here is the JAXRSClientFactoryBean, which creates an implementation of CustomerService that makes calls to the RESTful service running at localhost:9090.

What this means is that if you publish an artifact containing just the interface, your client-side project can make use of your RESTful service by directly calling the interface methods in Java, hiding the JAX-RS mechanics completely.

2 comments:

Unknown said...

What imports do you use

Unknown said...

I think these are the relevant ones.

In the service:

import javax.inject.Inject;
import javax.ws.rs.Produces;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.MediaType;

In the test:

import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
import org.codehaus.jackson.jaxrs.JacksonJsonProvider;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;