Wednesday, January 13, 2016

Создаем RESTful веб-сервис с помощью Jersey. Основы

REST (Representational State Transfer) - архитектурный подход, позволяющий создавать распределенные приложения.
Веб-сервисы, созданные согласно этому подходу, называются RESTful веб-сервисами.

Рассмотрим как создать приложение, предоставляющее такие веб-сервисы.

Принципы REST:
  • Ресурсы идентифицируются с помощью URI
  • Ресурсы могут иметь множество представлений
  • Ресурсы могут быть получены, созданы, изменены и удалены с помощью стандартных HTTP-методов
  • Сервер не хранит информацию о состоянии

Спроектируем приложение согласно этим принципам. Пусть оно позволяет работать с цитатами известных людей, т.е. цитаты - это наши ресурсы.
Определим HTTP-методы и URI для необходимых операций:

HTTP-метод URI Цель
GET /quotes/1 Получение цитаты
POST /quotes Создание цитаты
PUT /quotes/1 Изменение цитаты
DELETE /quotes/1 Удаление цитаты

В качестве представления ресурсов остановимся на формате JSON.

Для реализации веб-сервиса мы будем использовать:
  • Java 8
  • Jersey
  • Maven 3.3.3
  • Tomcat 8.0.30

Приступим к реализации! Добавим зависимости Jersey в pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>jersey-basics-tutorial</groupId>
    <artifactId>jersey-basics-tutorial</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jersey.version>2.22.1</jersey.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-moxy</artifactId>
            <version>${jersey.version}</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>tutorial</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19.1</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Для хранения информации о цитате создадим класс QuoteDto с полями author и text.
Добавим аннотацию @XmlRootElement, которая позволяет преобразовывать XML- и JSON-объекты в Java-объекты и наоборот:
package com.jersey.tutorial.dto;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class QuoteDto {

    private String author;
    private String text;

    public QuoteDto() {

    }

    public QuoteDto(String author, String text) {
        this.author = author;
        this.text = text;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

Создадим класс QuotesService, который будет иметь CRUD-методы для работы с объектами типа QuoteDto.
Для простоты примера мы не будем использовать базу данных, а будем хранить объекты в синхронизированной HashMap:
package com.jersey.tutorial.service;

import com.jersey.tutorial.dto.QuoteDto;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class QuotesService {

    private static final QuotesService INSTANCE = new QuotesService();
    private static final Map<Integer, QuoteDto> QUOTES = Collections.synchronizedMap(new HashMap<Integer, QuoteDto>());

    private Integer id = 0;

    public static QuotesService getInstance() {
        return INSTANCE;
    }

    public QuoteDto getQuote(Integer id) {
        return QUOTES.get(id);
    }

    public void addQuote(QuoteDto quote) {
        synchronized(QUOTES) {
            id++;
            QUOTES.put(id, quote);
        }
    }

    public void updateQuote(Integer id, QuoteDto updatedQuoteDto) {
        synchronized(QUOTES) {
            QuoteDto quoteDto = QUOTES.get(id);

            if (quoteDto != null) {
                QUOTES.put(id, updatedQuoteDto);
            }
        }
    }

    public void deleteQuote(Integer id) {
        QUOTES.remove(id);
    }
}


Создадим класс QuotesResource, описывающий работу с ресурсами.
Добавим классу аннотацию @Path, которая указывает URI, общий для всех методов.
Добавим следующие методы:
  • getQuote(), который позволяет получать цитату.
    Согласно разработанной ранее архитектуре нашего приложения он должен вызываться с помощью GET-запроса, поэтому добавим аннотацию @GET.
    Добавим аннотацию @Produces для того, чтобы ресурс возвращался в формате JSON.
    Добавим аннотацию @Path для указания URI метода.
    Для того, чтобы параметр URI id был доступен в методе getQuote(), добавим одноименному аргументу метода аннотацию @PathParam.
    При помощи сервиса метод получает объект QuoteDto и возвращает его, используя объект Response
  • addQuote(), который позволяет создавать цитату.
    Метод должен вызываться с помощью POST-запроса, поэтому добавим аннотацию @POST.
    Добавим аннотацию @Consumes для того, чтобы метод принимал данные в формате JSON.
    Метод не возвращает никаких данных, поэтому аннотация @Produces не нужна.
    URI метода совпадает с URI, общим для всех методов класса, поэтому аннотация @Path тоже не нужна
  • updateQuote(), который позволяет изменять цитату.
    Метод должен вызываться с помощью PUT-запроса, поэтому добавим аннотацию @PUT.
    Добавим аннотации @Consumes, @Path и @PathParam
  • deleteQuote(), который позволяет удалять цитату.
    Метод должен вызываться с помощью DELETE-запроса, поэтому добавим аннотацию @DELETE.
    Добавим аннотации @Path и @PathParam
package com.jersey.tutorial.ws;

import com.jersey.tutorial.dto.QuoteDto;
import com.jersey.tutorial.service.QuotesService;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/quotes")
public class QuotesResource {

    private QuotesService quotesService = QuotesService.getInstance();

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/{id}")
    public Response getQuote(@PathParam("id") Integer id) {
        QuoteDto quote = quotesService.getQuote(id);
        return Response.ok(quote).build();
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response addQuote(QuoteDto quoteDto) {
        quotesService.addQuote(quoteDto);
        return Response.ok().build();
    }

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Path("/{id}")
    public Response updateQuote(@PathParam("id") Integer id, QuoteDto quoteDto) {
        quotesService.updateQuote(id, quoteDto);
        return Response.ok().build();
    }

    @DELETE
    @Path("/{id}")
    public Response deleteQuote(@PathParam("id") Integer id) {
        quotesService.deleteQuote(id);
        return Response.ok().build();
    }
}

Для развертывания приложения создадим класс QuotesApplication, который наследует класс ResourceConfig.
Добавим аннотацию @ApplicationPath для указания пути развертывания приложения.
В конструкторе класса вызовем метод packages(), в который нужно передать имя пакета с классами нашего приложения:
package com.jersey.tutorial.ws;

import org.glassfish.jersey.server.ResourceConfig;

import javax.ws.rs.ApplicationPath;

@ApplicationPath("/")
public class QuotesApplication extends ResourceConfig {

    public QuotesApplication() {
        packages("com.jersey.tutorial");
    }
}

Соберем проект, выполнив команду "mvn clean install":
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building jersey-basics-tutorial 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ jersey-basics-tutorial ---
[INFO] Deleting C:\Blog\jersey-basics-tutorial\target
[INFO] 
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ jersey-basics-tutorial ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ jersey-basics-tutorial ---
[INFO] Compiling 4 source files to C:\Blog\jersey-basics-tutorial\target\classes
[INFO] 
[INFO] --- maven-resources-plugin:2.5:testResources (default-testResources) @ jersey-basics-tutorial ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Blog\jersey-basics-tutorial\src\test\resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ jersey-basics-tutorial ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.19.1:test (default-test) @ jersey-basics-tutorial ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-war-plugin:2.6:war (default-war) @ jersey-basics-tutorial ---
[INFO] Packaging webapp
[INFO] Assembling webapp [jersey-basics-tutorial] in [C:\Blog\jersey-basics-tutorial\target\tutorial]
[INFO] Processing war project
[INFO] Copying webapp resources [C:\Blog\jersey-basics-tutorial\src\main\webapp]
[INFO] Webapp assembled in [361 msecs]
[INFO] Building war: C:\Blog\jersey-basics-tutorial\target\tutorial.war
[INFO] 
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ jersey-basics-tutorial ---
[INFO] Installing C:\Blog\jersey-basics-tutorial\target\tutorial.war to C:\Users\alex\.m2\repository\jersey-basics-tutorial\jersey-basics-tutorial\1.0-SNAPSHOT\jersey-basics-tutorial-1.0-SNAPSHOT.war
[INFO] Installing C:\Blog\jersey-basics-tutorial\pom.xml to C:\Users\alex\.m2\repository\jersey-basics-tutorial\jersey-basics-tutorial\1.0-SNAPSHOT\jersey-basics-tutorial-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.545s
[INFO] Finished at: Fri Jan 15 15:40:47 EET 2016
[INFO] Final Memory: 17M/215M
[INFO] ------------------------------------------------------------------------

Скопируем полученный war-файл в директорию Tomcat'а webapps, запустим сервер и проверим его работу с помощью REST-клиента, например Postman.

Добавим цитату, выполнив POST-запрос:






















Получим цитату с помощью GET-запроса:



















Изменим цитату с помощью PUT-запроса:






















Проверим, что цитата изменилась с помощью GET-запроса:



















Удалим цитату, выполнив DELETE-запрос:














Проверим, что цитата удалена с помощью GET-запроса:


















Структура проекта:



Исходный код созданного приложения доступен по ссылке https://subversion.assembla.com/svn/jersey-basics-tutorial/trunk/.

Рекомендуемые посты:

No comments:

Post a Comment