22 February, 2012

Упрощаем работу с JPA при помощи Spring Data JPA

Введение

Уже прошло несколько лет с тех пор, как появился JPA. Работа с EntityManager увлекательна, но разработчики пишут красивый API, а подробности работы с базой данных скрывают. При этом частая проблема - дублирование имплементации, когда из одного DAO в другой у нас плавно перекочёвывает один и тот же код, в лучшем случае этот код переносится в абстрактный базовый DAO. Spring Data коренным образом решает проблему - при его использовании остаётся только API на уровне интерфейсов, вся имплементация создаётся автоматически с использованием AOP.

История Spring Data

Несмотря на то, что проект только недавно достиг версии 1.0, у него достаточно богатая история - раньше он развивался в рамках проекта Hades.

Объявление DAO-интерфейса

Итак, для начала нам необходимо объявить DAO-интерфейс, в котором мы будем объявлять методы для работы с сущностью.
public interface UserRepository extends CrudRepository<User, Long> {
}
Данного кода достаточно для обычного DAO с CRUD-методами.
  • save - сохраняет или обновляет переданную сущность.
  • findOne - ищет сущность по первичному ключу.
  • findAll - возвращает коллекцию всех сущностей
  • count - возвращает количество сущностей
  • delete - удаляет сущность
  • exists - проверяет, существует ли сущность с данным первичным ключом
Полный список методов, объявленный в CrudRepository можно посмотреть в javadoc. В случае, если нам нужны не все методы, то есть возможность произвести наследование от интерфейса Repository и перенести в наследника только те методы из интерфейса CrudRepository, которые нужны.

Поддержка сортировки и постраничного просмотра

Очень часто требующаяся функциональность - это возможность возвращать только часть сущностей из БД, например, для реализации постраничного просмотра в пользовательском интерфейсе. Spring Data и тут хорош и предоставляет нам возможность добавить такую функциональность в наш DAO. Для этого достаточно добавить объявление следующего метода в наш DAO-интерфейс:
 Page<User> findAll(Pageable pageable);
Интерфейс Pageable инкапсулирует в себе сведения о номере запрашиваемой страницы, размере страницы, а также требуемой сортировке.

Ищем данные

Как правило, на обычных CRUD-ах DAO не заканчиваются и часто требуются дополнительные методы, которые возвращают только те сущности, которые удовлетворяют заданным условиям. На мой взгляд, Spring Data сильно упрощает жизнь в данной области. Например, нам нужен методы для поиска пользователя по логину и по его e-mail адресу:
 User findByLogin(String login);
 User findByEmail(String email);
Все просто. В случае, если нужны более сложные условия для поиска, то и это тоже реализовано. Spring Data поддерживает следующие операторы:
  • Between
  • IsNotNull
  • NotNull
  • IsNull
  • Null
  • LessThan
  • LessThanEqual
  • GreaterThan
  • GreaterThanEqual
  • NotLike
  • Like
  • NotIn
  • In
  • Near
  • Within
  • Regex
  • Exists
  • IsTrue
  • True
  • IsFalse
  • False
  • Not
Такой внушительный список открывает простор для фантазии, так что можно составить сколь угодно сложный запрос. Если необходимо, чтобы в результатах поиска было несколько сущностей, то необходимо называть метод findAllByBlahBlah

Поддержка Spring MVC

Это часть основана на официальной документации. Представьте, что вы разрабатываете веб-приложение с использованием Spring MVC. Тогда вам необходимо будет загружать сущность из базы данных используя параметры HTTP-запроса. Это может выглядеть следующим образом:
@Controller
@RequestMapping("/users")
public class UserController {

  private final UserRepository userRepository;

  public UserController(UserRepository userRepository) {
    userRepository = userRepository;
  }

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") Long id, Model model) {
    
    // Do null check for id
    User user = userRepository.findOne(id);
    // Do null check for user
    // Populate model
    return "user";
  }
}
Во-первых, вы объявляете зависимость на DAO, а во-вторых всегда вызываете метод findOne() для загрузки сущности. К счастью, Spring позволяет нам преобразовывать строковые значения из HTTP-запросов в любой нужный тип используя либо PropertyEditor, либо ConversionService. Если вы используете Spring версии 3.0 и выше, то вам необходимо добавить следующую конфигурацию:
<mvc:annotation-driven conversion-service="conversionService" />
<bean id="conversionService" class="….context.support.ConversionServiceFactoryBean">
  <property name="converters">
    <list>
      <bean class="org.springframework.data.repository.support.DomainClassConverter">
        <constructor-arg ref="conversionService" />
      </bean>
    </list>
  </property>
</bean>
Если же вы используете Spring более старой версии, то вам необходима вот такая конфигурация:
<bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="webBindingInitializer">
    <bean class="….web.bind.support.ConfigurableWebBindingInitializer">
      <property name="propertyEditorRegistrars">
        <bean class="org.springframework.data.repository.support.DomainClassPropertyEditorRegistrar" />
      </property>
    </bean>
  </property>
</bean>
После данных изменений в конфигурации можно переписать контроллер следующим образом:

@Controller
@RequestMapping("/users")
public class UserController {

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") User user, Model model) {

    // Do null check for user
    // Populate model
    return "userForm";
  }
}
Обратите внимание на то, как упростился код и как мы красиво избавились от его дублирования.

Документация

На данный момент документации по проекту не так уж и много, но, тем не менее, она есть:

Заключение

Spring Data значительно упрощает жизнь при использовании JPA. Рекомендуется к использованию в своих проектах.

08 February, 2012

Разработка и тестирование 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>
2. Добавить 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>

Заключение

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