Oleg Atamanenko

thoughts about programming

Разработка и тестирование Java REST веб-сервисов

Введение

Для разработки REST веб-сервисов Java предлагает JSR-311 - JAX-RS: The Java™ API for RESTful Web Services Как это обычно бывает в мире Enterprise Java, существует несколько реализаций данной спецификации:На примере последней реализации, я и расскажу, каким образом можно написать REST-сервис.

Пишем REST-сервис


import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;

@Path("/service/entity")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public interface EntityRestService {

 @GET
 @Path("/all")
 EntityList listAll();

 @GET
 @Path("/{id}")
 Entity findById(@PathParam("id") Integer id);


}

Создаём интерфейс, в котором расставляем JAX-RS аннотации. Аннотация @Path указывает путь, по которому будет доступен наш сервис. Аннотация @GET определяет, какой HTTP-запрос будет обрабатываться данным методом. Аннотация @Produces позволяет указать, в каком формате данный сервис предоставляет результаты.

Конфигурация JBoss RESTEasy

Конфигурация очень проста, во-первых, нам нужно добавить обновить pom.xml и добавить необходимые зависимости:

Обновляем pom.xml

Добавляем compile-time зависимость на JAX-RS API

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>jaxrs-api</artifactId>
</dependency>

и runtime зависимости на реализацию

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <version>${resteasy-jaxrs.version}</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson-provider</artifactId>
    <version>${resteasy-jaxrs.version}</version>
    <scope>runtime</scope>
</dependency>
Обновляем конфигурацию веб-приложения в web.xml
Необходимо выполнить следующую модификацию web.xml
 <listener>
  <listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
 </listener>

 <context-param>
  <param-name>resteasy.servlet.mapping.prefix</param-name>
  <param-value>/rest</param-value>
 </context-param>
 <servlet>
  <servlet-name>REST Easy</servlet-name>
  <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
 </servlet>
 <servlet-mapping>
  <servlet-name>REST Easy</servlet-name>
  <url-pattern>/rest/*</url-pattern>
 </servlet-mapping>

Данный код объявляет сервлет HttpServletDispatcher, который будет обрабатывать все запросы, которые приходят на /rest/*. Слушатель ResteasyBootstrap выполняет всю необходимую инициализацию JBoss RESTEasy.

Поддержка Spring
Для того, чтобы использовать Spring Framework, нам необходимо сделать следующие изменения:
  1. Добавить зависимость в pom.xml
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-spring</artifactId>
    <version>${resteasy-jaxrs.version}</version>
    <scope>runtime</scope>
</dependency>
  1. Добавить Spring-ового слушателя в web.xml
 <listener>
  <listener-class>org.jboss.resteasy.plugins.spring.SpringContextLoaderListener</listener-class>
 </listener>

После этих изменений JBoss RESTEasy будет в курсе про Spring, это позволит в реализации REST-сервисов полноценно использовать все возможности, предоставляемые Spring Framework.

Пишем тесты

Написание тестов - весьма полезная вещь, ниже я покажу пример теста для REST-сервиса. В рамках данного теста у нас будет подниматься встраиваемый веб-сервер TJWSEmbeddedJaxrsServer, к которому мы будем обращаться для тестирования наших REST-сервисов.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/applicationContext-test.xml"})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class})
public class TestEntityServiceRest {

 private static final int PORT = 8081;
 private static final String BASE_URL = "http://localhost:" + PORT;

 @Autowired
 EntityService entityService;

 @Autowired
 ConfigurableApplicationContext applicationContext;


 protected HttpClient client;
 protected TJWSEmbeddedJaxrsServer server;


 @Before
 public void setUpClient() throws Exception {
  client = new DefaultHttpClient();
 }

 @Before
 public void setUpServer() throws Exception {
  server = new TJWSEmbeddedJaxrsServer();
  server.setPort(PORT);
  ResteasyDeployment deployment = server.getDeployment();

  server.start();

  Dispatcher dispatcher = deployment.getDispatcher();
  SpringBeanProcessor processor = new SpringBeanProcessor(dispatcher, deployment.getRegistry(), deployment.getProviderFactory());
  applicationContext.addBeanFactoryPostProcessor(processor);

  SpringResourceFactory noDefaults = new SpringResourceFactory(
    "entityServiceRestImpl", applicationContext, EntityRestServiceImpl.class);
  dispatcher.getRegistry().addResourceFactory(noDefaults);
 }

 @After
 public void stop() {
  server.stop();
 }

 @Test
 public void testListAll() throws Exception {

  int i = 0;
  final List<Entity> returnedList = new ArrayList<Entity>();
  int EXPECTED_SIZE = 6;
  while (i < EXPECTED_SIZE) {
   i++;
   final Entity entity = new Entity();
   entity.setId(i);
   entity.setName("test" + i);
   returnedList.add(entity);
  }
  when(entityService.findAll()).thenReturn(returnedList);


  HttpGet get = new HttpGet(BASE_URL + "/service/entity/");

  HttpResponse response = client.execute(get);
  InputStream content = response.getEntity().getContent();

  EntityList result = fromString(EntityList.class, content);
  content.close();

  Assert.assertNotNull(result);
  Assert.assertEquals(EXPECTED_SIZE, result.getEntities().size());
 }

 @Test
 public void testFindById() throws Exception {

  final Entity validEntity = getValidEntity();
  validEntity.setId(2);

  when(entityService.findById(validEntity.getId())).thenAnswer(new Answer<Object>() {
   @Override
   public Object answer(InvocationOnMock invocation) throws Throwable {
    return validEntity;
   }
  });

  HttpGet get = new HttpGet(BASE_URL + "/service/entity/2");

  HttpResponse response = client.execute(get);
  InputStream content = response.getEntity().getContent();

  Entity result = fromString(Entity.class, content);
  content.close();

  Assert.assertNotNull(result);

  deepAssertEquals(validEntity, result);

 }

 private void deepAssertEquals(Object expected, Object actual) throws IllegalAccessException, InvocationTargetException {
  Assert.assertEquals(expected.getClass(), actual.getClass());
  PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(expected.getClass());
  for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
   Object expectedValue = propertyDescriptor.getReadMethod().invoke(expected);
   Object actualValue = propertyDescriptor.getReadMethod().invoke(actual);
   Assert.assertEquals(expectedValue, actualValue);
  }
 }

        public static <T> T fromString(Class<T> clazz, String input) throws JAXBException {
  JAXBContext ctx = JAXBContext.newInstance(clazz);
  Unmarshaller unmarshaller = ctx.createUnmarshaller();
  @SuppressWarnings("unchecked")
  T unmarshal = (T) unmarshaller.unmarshal(new StringReader(input));
  return unmarshal;
 }

 public static <T> T fromString(Class<T> clazz, InputStream input) throws JAXBException {
  JAXBContext ctx = JAXBContext.newInstance(clazz);
  @SuppressWarnings("unchecked")
  T unmarshal = (T) ctx.createUnmarshaller().unmarshal(input);
  return unmarshal;
 }
}

Запуск тестов

Половина дела сделана, теперь нам необходимо настроить инфраструктуру для тестирования наших свеженаписанных REST-сервисов.

Для этого нам необходимо внести следующие модификации в pom.xml:


<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.11</version>

    <executions>
        <execution>
     <id>Unit tests</id>
            <phase>test</phase>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <excludes>
                    <exclude>**/IT*.java</exclude>
                </excludes>
            </configuration>
        </execution>
        <execution>
            <id>Integration tests</id>
            <phase>integration-test</phase>
            <goals>
                <goal>test</goal>
            </goals>
            <configuration>
                <includes>
                    <include>**/IT*.java</include>
                </includes>
            </configuration>
        </execution>
    </executions>
</plugin>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.1.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxrs</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-spring</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>tjws</artifactId>
    <version>${resteasy-jaxrs.version}</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <scope>test</scope>
</dependency>

Заключение

В целом, ничего сложного, главное делать всё аккуратно. Хорошего вам кода!