среда, 13 января 2016 г.

Пишем простой RESTful веб-сервис на Spring Web MVC

Суровый разместил на GitHub'е новый репозиторий, в котором будет собирать примеры использования Spring Framework 4.x. И сегодня я поделюсь с уважаемыми читателями блога примером простого RESTful веб-сервиса, реализованного на базе фреймворка Spring Web MVC и не содержащего ни строчки XML за исключением pom.xml.


Архитектура сервиса


Задачей примера было продемонстрировать реализацию классической многослойной архитектуры, к которой тяготеет большинство приложений, построенных на основе Spring Framework. Точкой входа является контроллер MessageController, в который инжектируются "сервисы" MessageService и ZShopService:


package psamolysov.demo.spring.restws.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import psamolysov.demo.spring.restws.model.ZShop;
import psamolysov.demo.spring.restws.service.MessageService;
import psamolysov.demo.spring.restws.service.ZShopService;

@RestController
public class MessageController {

 private MessageService messageService;
 
 private ZShopService zshopService;
 
 @Autowired
 public void setMessageService(MessageService service) {
  this.messageService = service;
 }
 
 @Autowired
 public void setZShopService(ZShopService service) {
  this.zshopService = service;
 }
 
 @RequestMapping(path = "/message", method = RequestMethod.GET, 
   produces = "text/plain")
 public String textMessage() {
  return messageService.textMessage();
 }
 
 @RequestMapping(path = "/message", method = RequestMethod.GET, 
   produces = "application/json")
 public ZShop jsonMessage() {
  return zshopService.getRandomShop();
 }
 
 @RequestMapping(path = "/message", method = RequestMethod.GET, 
   produces = "text/xml")
 public ZShop xmlMessage() { 
  return zshopService.getRandomShop();
 }
}

В данные сервисы в свою очередь инжектируются т.н. "ресурсы", в данном примере - TextResource. Данный ресурс можно рассматривать как пример примитивного DAO.

MessageService:

package psamolysov.demo.spring.restws.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import psamolysov.demo.spring.restws.model.TextResource;

@Service
public class MessageService {

 private TextResource textResource;
 
 @Autowired
 public void setTextResource(TextResource textResource) {
  this.textResource = textResource;
 }
 
 public String textMessage() {
  return textResource.message();
 }
}

TextResource:

package psamolysov.demo.spring.restws.model;

import org.springframework.stereotype.Component;

@Component
public class TextResource {

 public String message() {
  return "Message#" + System.currentTimeMillis();
 }
}

Зависимости в контроллере и сервисах описываются с помощью аннотации Autowired. Если в дальнейшем нужно будет написать модульные тесты на классы приложения, то для облегчения внедрения зависимостей в каждом классе созданы соответствующие публичные сеттеры.

Контроллер


Задачей контроллера является взаимодействие с клиентом. Он должен принять HTTP-запрос с нужным методом (GET, POST, PUT, DELETE и т.д.) и заданными ограничениями (Accept:...), извлечь из него параметры, обратиться к модели, получить от нее результат, преобразовать этот результат в соответствии с ожиданиями пользователя и возвратить ему. Большую часть работы берет на себя Spring Web MVC, разработчикам остается только декларативно с помощью аннотаций описать свои ожидания, отображение запросов на методы контроллера и требуемое представление для возвращаемых результатов.

В случае RESTful веб-сервиса возвращаемый результат является телом ответа, а не объектом класса ModelAndView, таким образом для регистрации контроллера в Spring Web MVC фреймворке служит аннотация RestController.

Контроллер демонстрационного приложения содержит три метода: textMessage(), jsonMessage() и xmlMessage(). Каждый из методов отображается на общий путь /api/message, диспетчеризация осуществляется по типу запрашиваемых данных, который передается в заголовке Accept HTTP-запроса. Данное поведение настраивается с помощью свойства produces аннотации RequestMapping.

@RequestMapping(path = "/message", method = RequestMethod.GET, 
 produces = "application/json")
public ZShop jsonMessage() {
 return zshopService.getRandomShop();
}

Формат возвращаемых каждым методом данных понятен из его названия.

Настройка контекста приложения


Настройка контекста приложения Spring Framework осуществляется с помощью Java-кода. Сначала необходимо зарегистрировать основной сервлет Spring Web MVC - DispatcherServlet, настроить его отображение на путь /api/*, а так же описать необходимый для работы сервлета слушатель ContextLoaderListener, загружающий контекст приложения Spring Framework. Вся эта работа выполняется в классе ApplicationInitializer, являющемся по сути современной заменой файлу web.xml.

package psamolysov.demo.spring.restws;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class ApplicationInitializer implements WebApplicationInitializer {

 @Override
 public void onStartup(ServletContext servletContext) throws ServletException {
  AnnotationConfigWebApplicationContext applicationContext = 
    new AnnotationConfigWebApplicationContext();
  applicationContext.register(ApplicationConfig.class);  
  
  servletContext.addListener(new ContextLoaderListener(applicationContext));
  
  ServletRegistration.Dynamic dispatcher = 
    servletContext.addServlet("spring-mvc-dispatcher", 
      new DispatcherServlet(applicationContext));
  dispatcher.setLoadOnStartup(1);
  dispatcher.addMapping("/api/*");  
 }
}

В первой строчке метода onStartup() регистрируется класс, который задает конфигурацию фреймворка Spring Web MVC. Данный класс может наследоваться от базового класса WebMvcConfigurerAdapter и при необходимости переопределять его методы для тонкой настройки фреймворка. Ниже мы рассмотрим зачем это может понадобиться.

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
@EnableWebMvc
public class ApplicationConfig extends WebMvcConfigurerAdapter { 
}

В данном примере аннотация ComponentScan задает базовый пакет, в котором Spring Framework должен искать зависимости и точки их инжекции, а аннотация EnableWebMvc говорит о том, что конфигурация фреймворка Spring Web MVC должна быть унаследована от WebMvcConfigurationSupport.

Как Spring MVC трансформирует объекты в JSON и XML


Spring Web MVC конвертирует тело HTTP-запроса в аргументы метода контроллера с помощью HttpMessageConverter. Так же данный класс отвечает за преобразование объектов в тело HTTP-ответа. По-умолчанию при использовании конфигурации MVC регистрируются следующие конвертеры:

  • ByteArrayHttpMessageConverter - работает с массивами байт;

  • StringHttpMessageConverter - работает со строками;

  • ResourceHttpMessageConverter - работает с объектами класса org.springframework.core.io.Resource для всех типов медиа;

  • SourceHttpMessageConverter - работает с объектами класса javax.xml.transform.Source;

  • FormHttpMessageConverter - работает с объектами класса MultiValueMap<String, String>;

  • Jaxb2RootElementHttpMessageConverter - преобразует объекты Java в/из XML, если JAXB2 присутствует в classpath приложения, а библиотека Jackson 2 XML - отсутствует в classpath;

  • MappingJackson2HttpMessageConverter - преобразует объекты Java в/из JSON, если библиотека Jackson 2 присутствует в classpath приложения;

  • MappingJackson2XmlHttpMessageConverter - преобразует объекты Java в/из XML, если библиотека Jackson 2 XML присутствует в classpath приложения;

  • AtomFeedHttpMessageConverter - работает с потоками Atom, если библиотека Rome присутствует в classpath приложения;

  • RssChannelHttpMessageConverter - работает с потоками RSS, если библиотека Rome присутствует в classpath приложения.

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

Расширение библиотеки Jackson 2 для работы с XML может обрабатывать JAXB2 аннотации. Однако соответствующий конвертер для этого необходимо настроить: включить JaxbAnnotationIntrospector. Настройка конвертеров производится в методе configureMessageConverters() класса конфигурации приложения (наследника WebMvcConfigurerAdapter).

package psamolysov.demo.spring.restws;

import java.util.List;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
@EnableWebMvc
public class ApplicationConfig extends WebMvcConfigurerAdapter { 

 @Override
 public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
  Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder = new Jackson2ObjectMapperBuilder()
    .annotationIntrospector(new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()))
    .indentOutput(true);
  converters.add(new StringHttpMessageConverter());
  converters.add(new MappingJackson2HttpMessageConverter());
  converters.add(new MappingJackson2XmlHttpMessageConverter(
    jacksonObjectMapperBuilder
     .createXmlMapper(true)
     .build()));
 } 
 
 
}

Теперь объекты класса ZShop, в котором активно используются JAXB2 аннотации,

package psamolysov.demo.spring.restws.model;

import java.io.Serializable;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "zshop", namespace = "http://ibm.com/ru/spring-integration/demo/workspace")
public class ZShop implements Serializable {

 private static final long serialVersionUID = -6371123466652190899L;

 private String orderName;
  
 private int orderCount;
 
 public ZShop() {  
 }
 
 public ZShop(String orderName, int orderCount) {
  this.orderName = orderName;
  this.orderCount = orderCount;
 }

 @XmlAttribute
 public String getOrderName() {
  return orderName;
 }

 public void setOrderName(String orderName) {
  this.orderName = orderName;
 }

 @XmlAttribute
 public int getOrderCount() {
  return orderCount;
 }

 public void setOrderCount(int orderCount) {
  this.orderCount = orderCount;
 } 
}


будут преобразованы в такой XML:

<zshop orderName="IBM zShop" orderCount="648" xmlns="http://ibm.com/ru/spring-integration/demo/workspace"></zshop>

Развертывание и тестирование приложения


Проект настроен таким образом, чтобы его можно было легко и просто развернуть на автоматически загружаемом из репозитория образе сервера приложений WebSphere Liberty Profile. Соответственно, необходимо выполнить буквально три команды.

1. Загрузить образ сервера приложений из репозитория. Архив с ядром Liberty profile занимает всего 11Мб, для сравнения - Apache Tomcat 8.0.30 - 9.5Мб, т.е. всего на 1.5 Мб меньше.

$ mvn liberty:create-server

2. Установить единственную необходимую для запуска примера возможность (feature) - servlet-3.1.

$ mvn liberty:install-feature

3. Запустить сервер с примером на исполнение.

$ mvn liberty:run-server

Сервер приложений WebSphere Liberty profile так же прозрачно интегрируется со средой разработки Eclipse SDK.

Так как для тестирования всех методов контроллера критически важно передать правильный заголовок Accept, то нужно написать небольшую программку, например, с использованием Jersey 2.

package psamolysov.demo.spring.restws.client;

import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;

public class SimpleInvokator {

 public static void main(String[] args) {
        WebTarget target = ClientBuilder.newClient().target(
          "http://localhost:9082/rest-web-service");
        
        String restext = target
                .path("/api/message")                
                .request()
                .accept("text/plain")
                .get(String.class);                
        System.out.println(restext);
        
        String resjson = target
                .path("/api/message")              
                .request()
                .accept("application/json")
                .get(String.class);                
        System.out.println(resjson);
        
        String resxml = target
                .path("/api/message")                
                .request()
                .accept("text/xml")
                .get(String.class);                
        System.out.println(resxml);
 }
}

Результат работы веб-сервиса должен выглядеть следующим образом:

Message#1452523665973
{"orderName":"IBM zShop","orderCount":441}
<zshop orderName="IBM zShop" orderCount="648" xmlns="http://ibm.com/ru/spring-integration/demo/workspace"></zshop>

Выводы


В настоящее время в связи с появлением достаточно быстрых движков Java Script набирает популярность подход к построению веб-приложений, основанный на полном разделении клиента и сервера. Клиентом выступает отдельная веб-страница, которая все свое взаимодействие с сервером осуществляет посредством обращения к RESTful веб-сервисам. Клиент и сервер в этой новой парадигме обычно обмениваются данными в формате JSON, т.к. это - родной формат для языка Java Script. Выставленные веб-сервисы так же могут использоваться мобильными клиентами, работающими на различных платформах (iOS, Android, что-то еще?), в таком случае может быть предпочтительнее использовать XML. Отрадно, что фреймворк Spring Web MVC берет всю работу о конвертации объектов в соответствующие форматы обмена данными на себя, освобождая разработчиков от заботы об этом.

Надеюсь данный пример оказался полезным для вас. В любом случае вы всегда можете высказать свое мнение в комментариях. Добро пожаловать!

Понравилось сообщение - подпишитесь на блог

9 комментариев:

ilinchik комментирует...

Раз уж в примере используется настройка при помощью Java-кода без web.xml в очередной раз задам вопрос про инициализацию приложения. Мне интересно как контейнер-сервлетов находит класс ApplicationInitializer? Т.е контейнеру нужно найти реализации WebApplicationInitializer, неужели он загружает все классы из classpath (уверен что это не так) и в случае большого кол-ва зависимостей не будет ли такая конфигурация влиять на время деплоймента?

Pavel Samolisov комментирует...

@ilinchik Я начал отвечать на вопрос и понял, что получается много, поэтому хочу вынести в отдельный пост. Получается много, поэтому пока не публикую.

Если кратко, то ответ на SO вполне корректный и довольно прозрачный. Я попробовал зарегистрировать свои листенеры, у меня получилось. Работает и на WebSphere Liberty Profile и на Apache Tomcat.

Pavel Samolisov комментирует...

Ответил на вопрос.

ZeroSetup комментирует...

Вот еще как можно просто конвертировать в JSON:

Latest Jackson integration improvements in Spring

https://spring.io/blog/2014/12/02/latest-jackson-integration-improvements-in-spring

Pavel Samolysov комментирует...

Большое спасибо за ссылку!

Unknown комментирует...

Почему не стали добавлять интерфейсы для сервиса и компонентов?
Хорошее объяснение зачем нужно работать с интерфейсами:
https://youtu.be/U8MtGYa04v8?t=2664

Pavel Samolysov комментирует...

Здесь приведен довольно примитивный пример: две реализации сервисов, причем не приводимые к общему интерфейсу и по одной реализации ресурса. Если нужно будет добавить еще пару реализаций сервисов, пересекающихся по методам, то всегда можно выполнить рефакторинг "выделение интерфейса". Только вот если честно, в реальном коде постоянно наблюдаются выделенные интерфейсы сервисов, каждый из которых имеет ровно одну реализацию, обычно содержащую набор методов типа book(), cancelBooking(), getAllBookings() и т.д. Это или карго-культ какой-то, или ограничения используемых фреймворков, позволяющих навешивать, например, декларативное управление транзакциями только на интерфейсы. Или делать моки только над интерфейсами. Или как в Java EE (J2EE) до появления EJB 3.1 спецификация требовала, чтобы каждый сессионный компонент реализовывал или локальный, или удаленный интерфейс.

Осознанный смысл в интерфейсах появляется, когда размещают логику по компонентам немного хитрее. Мое любимое паттерн QueryObject (пример от Сурового) или бизнес-экшны, реализующие некий интерфейс с одним методом execute() (иногда дополняемым методами preExecute() и/или postExecute()).

Unknown комментирует...

Где бы скачать исходники? Просто геморойно собирать проект по крупицам из статьи - однозначно что-то не так сделаешь и проект не будет работать

Pavel Samolysov комментирует...

Ссылка на репозиторий: https://github.com/samolisov/spring-4x-demos

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!