Статьи

Учебник — проектирование и реализация API REST на Java с Джерси и Спрингом

Глядя на отдых в Java? Тогда вы попали в нужное место, потому что в посте блога я расскажу вам, как «красиво» спроектировать REST API, а также, как реализовать его в Java с помощью среды Jersey. API RESTful, разработанный в этом руководстве, продемонстрирует полную функциональность Create, _read, _update_and_delete (CRUD) для ресурсов подкастов, хранящихся в базе данных MySql.

1. Пример

1.1. Почему?

Прежде чем мы начнем, позвольте мне рассказать вам, почему я написал этот пост. Что ж , я собираюсь предложить в будущем REST API для Podcastpedia.org . Конечно, я мог бы использовать собственную реализацию REST Spring , как я сейчас использую для вызовов AJAX, но я также хотел посмотреть, как выглядит «официальная» реализация. Таким образом, лучший способ познакомиться с технологией — создать прототип с ее использованием. Это то, что я сделал и что я представляю здесь, и я могу сказать, что я чертовски доволен Джерси. Читайте дальше, чтобы понять почему !!!

Примечание: вы можете посетить окно моего автозаполнения с jQuery и Spring MVC, чтобы увидеть, как Spring обрабатывает REST-запросы.

1.2. Что оно делает?

Ресурсом, управляемым в этом руководстве, являются подкасты. API REST позволит создавать, извлекать, обновлять и удалять такие ресурсы.

1.3. Архитектура и технологии

Rest-Демо-схема

Демонстрационное приложение использует многоуровневую архитектуру, основанную на «Законе Деметры (LoD) или принципе наименьших знаний» [16] :

  • Первый уровень — это поддержка REST, реализованная с помощью Jersey, выполняет роль фасада и делегирует логику бизнес-уровню.
  • бизнес-уровень , где логика происходит
  • уровень доступа к данным — это место, где происходит связь с хранилищем данных (в нашем случае база данных MySql)

Несколько слов об используемых технологиях / структурах:

1.3.1. Джерси (Фасад)

Инфраструктура RESTful Web Services Jersey представляет собой среду с открытым исходным кодом, качество производства, среду разработки RESTful Web Services на Java, которая обеспечивает поддержку API JAX-RS и служит эталонной реализацией JAX-RS (JSR 311 & JSR 339).

1.3.2. Весна (Деловой уровень)

Мне нравится склеивать вещи вместе с Spring , и этот пример не исключение. На мой взгляд, нет лучшего способа сделать POJO с различными функциями. В руководстве вы узнаете, что нужно для интеграции Jersey 2 с Spring.

1.3.3. JPA 2 / Hibernate (постоянный слой)

Для уровня персистентности я все еще использую шаблон DAO, хотя для его реализации я использую JPA 2, который, как некоторые люди говорят, должен сделать DAO излишним (мне, например, не нравятся мои классы обслуживания, загроможденные EntityManager / JPA конкретный код). В качестве поддерживающей платформы для JPA 2 я использую Hibernate.

Смотрите мой пост Пример Java Persistence с Spring, JPA2 и Hibernate для интересного обсуждения темы персистентности в Java.

1.3.4. Веб-контейнер

Все упаковано с Maven в виде файла .war и может быть развернуто в любом веб-контейнере — я использовал Tomcat и Jetty, но это также может быть Glassfih, Weblogic, JBoss или WebSphere.

1.3.5. MySQL

Пример данных хранится в таблице MySQL :

базы данных схемы

1.3.6. Технологические версии

  1. Джерси 2.9
  2. Spring 4.0.3
  3. Hibernate 4
  4. Maven 3
  5. Tomcat 7
  6. Причал 9
  7. MySql 5.6

Примечание . Основное внимание в посте будет уделено разработке API REST и его реализации с использованием JAX-RS в Джерси, а все остальные технологии / уровни рассматриваются как средства поддержки.

1.4. Исходный код

Исходный код проекта, представленный здесь, доступен на GitHub с подробными инструкциями по установке и запуску проекта:

2. Конфигурация

Прежде чем я начну представлять дизайн и реализацию REST API, нам нужно немного настроить, чтобы все эти замечательные технологии могли объединиться и играть вместе.

2.1. Зависимости проекта

Расширение Jersey Spring должно присутствовать в classpath вашего проекта. Если вы используете Maven, добавьте его в файл pom.xml вашего проекта:

Джерси-весна зависимость в pom.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-spring3</artifactId>
    <version>${jersey.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </exclusion>         
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </exclusion>
    </exclusions>        
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.4.1</version>
</dependency>

Примечание: jersey-spring3.jar использует свою собственную версию для библиотек Spring, поэтому, чтобы использовать те, которые вы хотите (Spring 4.0.3. В этом случае — выпуск), вам необходимо исключить эти библиотеки вручную.

Предупреждение кода: Если вы хотите увидеть, какие другие зависимости нужны (например, Spring, Hibernate, плагин Jetty maven, тестирование и т. Д.) В проекте, вы можете посмотреть полный файл pom.xml, доступный на GitHub.

2.2. web.xml

Дескриптор развертывания веб-приложения

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
    <display-name>Demo - Restful Web Application</display-name>
 
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
 
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/applicationContext.xml</param-value>
    </context-param>
 
    <servlet>
        <servlet-name>jersey-serlvet</servlet-name>
        <servlet-class>
            org.glassfish.jersey.servlet.ServletContainer
        </servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>org.codingpedia.demo.rest.RestDemoJaxRsApplication</param-value>          
        </init-param>    
        <load-on-startup>1</load-on-startup>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>jersey-serlvet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
 
    <resource-ref>
        <description>Database resource rest demo web application </description>
        <res-ref-name>jdbc/restDemoDB</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>  
</web-app>

2.2.1. Джерси-сервлет

Обратите внимание на конфигурацию сервлета Джерси [строки 18-33]. Класс javax.ws.rs.core.Application определяет компоненты (классы корневых ресурсов и поставщиков) приложения JAX-RS. Я использовал ResourceConfig, который является собственной реализацией класса Application Джерси и предоставляет расширенные возможности для упрощения регистрации компонентов JAX-RS. Посмотрите Модель Приложения JAX-RS в документации для большего количества возможностей.

Моя реализация класса ResourceConfig , org.codingpedia.demo.rest.RestDemoJaxRsApplication , регистрирует ресурсы приложения, фильтры, средства отображения исключений и функции:

org.codingpedia.demo.rest.service.MyDemoApplication

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package org.codingpedia.demo.rest.service;
 
//imports omitted for brevity
 
/**
 * Registers the components to be used by the JAX-RS application
 *
 * @author ama
 *
 */
public class RestDemoJaxRsApplication extends ResourceConfig {
 
    /**
     * Register JAX-RS application components.
     */
    public RestDemoJaxRsApplication() {
        // register application resources
        register(PodcastResource.class);
        register(PodcastLegacyResource.class);
 
        // register filters
        register(RequestContextFilter.class);
        register(LoggingResponseFilter.class);
        register(CORSResponseFilter.class);
 
        // register exception mappers
        register(GenericExceptionMapper.class);
        register(AppExceptionMapper.class);
        register(NotFoundExceptionMapper.class);
 
        // register features
        register(JacksonFeature.class);
        register(MultiPartFeature.class);
    }
}

Пожалуйста, обратите внимание:

  • org.glassfish.jersey.server.spring.scope.RequestContextFilter , который является фильтром Spring, обеспечивающим мост между атрибутами JAX-RS и Spring.
  • org.codingpedia.demo.rest.resource.PodcastsResource , который является «фасадным» компонентом, который предоставляет REST API через аннотации и будет подробно представлен позже в посте.
  • org.glassfish.jersey.jackson.JacksonFeature , функция, которая регистрирует JSON-провайдеров — она ​​нужна приложению для понимания данных JSON

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

Конфигурация контекста приложения Spring находится в classpath в spring/applicationContext.xml :

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    xsi:schemaLocation="
 
 
 
    <context:component-scan base-package="org.codingpedia.demo.rest.*" />
 
    <!-- ************ JPA configuration *********** -->
    <tx:annotation-driven transaction-manager="transactionManager" /> 
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
    <bean id="transactionManagerLegacy" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactoryLegacy" />
    </bean>   
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" />
        <property name="persistenceUnitName" value="demoRestPersistence" />       
        <property name="dataSource" ref="restDemoDS" />
        <property name="packagesToScan" value="org.codingpedia.demo.*" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true" />
                <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
            </bean>
        </property>
    </bean>    
    <bean id="entityManagerFactoryLegacy" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" />
        <property name="persistenceUnitName" value="demoRestPersistenceLegacy" />
        <property name="dataSource" ref="restDemoLegacyDS" />
        <property name="packagesToScan" value="org.codingpedia.demo.*" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true" />
                <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
            </bean>
        </property>
    </bean>       
 
    <bean id="podcastDao" class="org.codingpedia.demo.rest.dao.PodcastDaoJPA2Impl"/> 
    <bean id="podcastService" class="org.codingpedia.demo.rest.service.PodcastServiceDbAccessImpl" />
    <bean id="podcastsResource" class="org.codingpedia.demo.rest.resource.PodcastsResource" />
    <bean id="podcastLegacyResource" class="org.codingpedia.demo.rest.resource.PodcastLegacyResource" />
 
    <bean id="restDemoDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton">
        <property name="jndiName" value="java:comp/env/jdbc/restDemoDB" />
        <property name="resourceRef" value="true" />       
    </bean>
    <bean id="restDemoLegacyDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton">
        <property name="jndiName" value="java:comp/env/jdbc/restDemoLegacyDB" />
        <property name="resourceRef" value="true" />       
    </bean>  
</beans>

Здесь нет ничего особенного, он просто определяет компоненты, необходимые для демонстрационного приложения (например, podcastsResource который является классом точки входа для нашего REST API).

3. REST API (разработка и реализация)

3.1. Ресурсы

3.1.1. дизайн

Как упоминалось ранее, демонстрационное приложение управляет подкастами, которые представляют ресурс в нашем REST API. Ресурсы являются центральной концепцией в REST и характеризуются двумя основными вещами:

  • на каждый ссылается глобальный идентификатор (например, URI в HTTP).
  • имеет одно или несколько представлений, которые они открывают для внешнего мира и которыми можно манипулировать (в этом примере мы будем работать в основном с представлениями JSON)

Ресурсы обычно представлены в REST существительными (подкасты, клиенты, пользователи, учетные записи и т. Д.), А не глаголами (getPodcast, deleteUser и т. Д.)

Конечные точки, используемые на протяжении всего урока:

  • /podcasts(обратите внимание на множественное число) URI, идентифицирующий ресурс, представляющий коллекцию подкастов
  • /podcasts/{id} — URI, идентифицирующий ресурс подкаста, по идентификатору подкаста

3.1.2. Реализация

Для простоты подкаст будет иметь только следующие свойства:

  • id — уникально идентифицирует подкаст
  • feed — url-канал подкаста
  • title — название подкаста
  • linkOnPodcastpedia — где вы можете найти подкаст на Podcastpedia.org
  • description — краткое описание подкаста

Я мог бы использовать только один класс Java для представления ресурса подкаста в коде, но в этом случае класс и его свойства / методы были бы загромождены аннотациями JPA и XML / JAXB / JSON. Я хотел избежать этого, и я использовал два представления, которые имеют почти одинаковые свойства:

  • PodcastEntity.java — аннотированный класс JPA, используемый в БД и бизнес-уровнях
  • Podcast.java — аннотированный класс JAXB / JSON, используемый на фасаде и бизнес-уровнях

Примечание: я все еще пытаюсь убедить себя, что это лучший подход, поэтому, если у вас есть предложения по этому вопросу, пожалуйста, оставьте комментарий.

Класс Podcast.java выглядит примерно так:

Podcast.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package org.codingpedia.demo.rest.resource;
 
//imports omitted for brevity
 
/**
 * Podcast resource placeholder for json/xml representation
 *
 * @author ama
 *
 */
@SuppressWarnings("restriction")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Podcast implements Serializable {
 
    private static final long serialVersionUID = -8039686696076337053L;
 
    /** id of the podcast */
    @XmlElement(name = "id")    
    private Long id;
     
    /** title of the podcast */
    @XmlElement(name = "title")    
    private String title;
         
    /** link of the podcast on Podcastpedia.org */
    @XmlElement(name = "linkOnPodcastpedia")    
    private String linkOnPodcastpedia;
     
    /** url of the feed */
    @XmlElement(name = "feed")    
    private String feed;
     
    /** description of the podcast */
    @XmlElement(name = "description")
    private String description;
         
    /** insertion date in the database */
    @XmlElement(name = "insertionDate")
    @XmlJavaTypeAdapter(DateISO8601Adapter.class)    
    @PodcastDetailedView
    private Date insertionDate;
 
    public Podcast(PodcastEntity podcastEntity){
        try {
            BeanUtils.copyProperties(this, podcastEntity);
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
     
    public Podcast(String title, String linkOnPodcastpedia, String feed,
            String description) {
         
        this.title = title;
        this.linkOnPodcastpedia = linkOnPodcastpedia;
        this.feed = feed;
        this.description = description;
         
    }
     
    public Podcast(){}
 
//getters and setters now shown for brevity
}

и переводится в следующее представление JSON, которое фактически является типом носителя de facto, используемым в настоящее время с REST:

1
2
3
4
5
6
7
8
{
    "id":1,
    "title":"Quarks & Co - zum Mitnehmen-modified",
    "description":"Quarks & Co: Das Wissenschaftsmagazin",
    "insertionDate":"2014-05-30T10:26:12.00+0200"
}

Несмотря на то, что JSON становится все более предпочтительным представлением в API-интерфейсах REST, не следует пренебрегать представлением XML, поскольку большинство систем по-прежнему используют формат XML для связи с другими сторонами.

Хорошо, что в Джерси вы можете убить двух кроликов одним выстрелом — с помощью бобов JAXB (как описано выше) вы сможете использовать ту же модель Java для генерации JSON, а также представления XML. Еще одним преимуществом является простота работы с такой моделью и доступность API в Java SE Platform.

Примечание. Большинство методов, определенных в этом руководстве, также создают и используют тип носителя application / xml, причем application / json является предпочтительным способом.

3.2. методы

Прежде чем я представлю вам API, позвольте мне сказать вам, что

  • Создать = ПОСТ
  • Читать = ПОЛУЧИТЬ
  • Обновление = PUT
  • Удалить = УДАЛИТЬ

и не является строгим отображением 1: 1. Почему? Потому что вы также можете использовать PUT для создания и POST для обновления. Это будет объяснено и продемонстрировано в следующих параграфах.

Примечание: для Read и Delete довольно ясно, они действительно отображаются один в один с помощью HTTP-операций GET и DELETE. В любом случае REST — это архитектурный стиль, а не спецификация, и вы должны адаптировать архитектуру к вашим потребностям, но если вы хотите сделать свой API общедоступным и иметь кого-то, кто хочет его использовать, вы должны следовать некоторым «наилучшим методам».

Как уже упоминалось, класс PodcastRestResource обрабатывает все остальные запросы:

01
02
03
04
05
06
07
08
09
10
package org.codingpedia.demo.rest.resource;
//imports
......................
@Component
@Path("/podcasts")
public class PodcastResource {
    @Autowired
    private PodcastService podcastService;
    .....................
}

Обратите внимание на @Path("/podcasts") перед определением класса — все, что связано с ресурсами подкаста, будет происходить по этому пути. Значение аннотации @Path — это относительный путь URI. В приведенном выше примере класс Java будет размещен в пути /podcasts URI. Интерфейс PodcastService предоставляет бизнес-логику фасадному слою REST.

Предупреждение кода: вы можете найти все содержимое класса на GitHub — PodcastResource.java . Мы рассмотрим файл шаг за шагом и объясним различные методы, соответствующие различным операциям.

3.2.1. Создать подкаст (ы)

3.2.1.1. дизайн

Хотя «наиболее известным» способом создания ресурса является использование POST, как уже упоминалось ранее, для создания нового ресурса я мог бы использовать как методы POST, так и PUT, и я сделал именно это:

Описание URI HTTP метод
HTTP Status response
Добавить новый подкаст / Подкасты / ПОЧТА 201 Создано
Добавить новый подкаст (все значения должны быть отправлены) / подкасты / {ID} ПОЛОЖИТЬ 201 Создано

Большая разница между использованием POST (не идемпотент)

«Метод POST используется для запроса, чтобы исходный сервер принял объект, заключенный в запросе, в качестве нового подчиненного ресурса, идентифицируемого Request-URI в строке запроса […] Если ресурс был создан на исходном сервере ответ ДОЛЖЕН быть 201 (Создан) и содержать объект, который описывает состояние запроса и ссылается на новый ресурс, и заголовок Location »[1]

и PUT (идемпотент)

«Метод PUT запрашивает, чтобы вложенный объект был сохранен под предоставленным Request-URI […] Если Request-URI не указывает на существующий ресурс, и этот URI может быть определен как новый ресурс запрашивающим пользовательским агентом исходный сервер может создать ресурс с этим URI. Если новый ресурс создан, сервер происхождения ДОЛЖЕН проинформировать об этом агента пользователя через ответ 201 (Создано) ». [1]

в том, что для PUT вы должны заранее знать место, где будет создан ресурс, и отправить все возможные значения записи.

3.2.1.2. Реализация

3.2.1.2.1. Создать единый ресурс с POST

Создать один ресурс подкаста из JSON

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 * Adds a new resource (podcast) from the given json format (at least title
 * and feed elements are required at the DB level)
 *
 * @param podcast
 * @return
 * @throws AppException
 */
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response createPodcast(Podcast podcast) throws AppException {
    Long createPodcastId = podcastService.createPodcast(podcast);
    return Response.status(Response.Status.CREATED)// 201
            .entity("A new podcast has been created")
            .header("Location",
                    "http://localhost:8888/demo-rest-jersey-spring/podcasts/"
                            + String.valueOf(createPodcastId)).build();
}

Аннотации

  • @POST — указывает, что метод отвечает на запросы HTTP POST
  • @Consumes({MediaType.APPLICATION_JSON}) — определяет тип мультимедиа, который принимает метод, в данном случае "application/json"
  • @Produces({MediaType.TEXT_HTML}) — определяет тип носителя), который может создать метод, в данном случае "text/html" .

отклик

  • в случае успеха: текстовый / html-документ с HTTP-статусом 201 Created и заголовком Location, указывающим, где был создан ресурс
  • по ошибке:
    • 400 Bad request если недостаточно данных
    • 409 Conflict если на стороне сервера определен подкаст с таким же фидом

3.2.1.2.2. Создать единый ресурс («подкаст») с PUT

Это будет рассмотрено в разделе «Обновление подкаста» ниже.

3.2.1.2.3. Бонус — создание единого ресурса («подкаста») из формы

Создать один ресурс подкаста из формы

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * Adds a new podcast (resource) from "form" (at least title and feed
 * elements are required at the DB level)
 *
 * @param title
 * @param linkOnPodcastpedia
 * @param feed
 * @param description
 * @return
 * @throws AppException
 */
@POST
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
@Produces({ MediaType.TEXT_HTML })
@Transactional
public Response createPodcastFromApplicationFormURLencoded(
        @FormParam("title") String title,
        @FormParam("linkOnPodcastpedia") String linkOnPodcastpedia,
        @FormParam("feed") String feed,
        @FormParam("description") String description) throws AppException {
 
    Podcast podcast = new Podcast(title, linkOnPodcastpedia, feed,
            description);
    Long createPodcastid = podcastService.createPodcast(podcast);
 
    return Response
            .status(Response.Status.CREATED)// 201
            .entity("A new podcast/resource has been created at /demo-rest-jersey-spring/podcasts/"
                    + createPodcastid)
            .header("Location",
                    "http://localhost:8888/demo-rest-jersey-spring/podcasts/"
                            + String.valueOf(createPodcastid)).build();
}

Аннотации

    • @POST — указывает, что метод отвечает на запросы HTTP POST
    • @Consumes({MediaType.APPLICATION_FORM_URLENCODED}) — определяет тип носителя, который принимает метод, в данном случае "application/x-www-form-urlencoded"
      • @FormParam — присутствует перед входными параметрами метода, эта аннотация связывает значение (я) параметра формы, содержащегося в теле объекта запроса, с параметром метода ресурса. Значения декодируются по URL, если только это не отключено с помощью аннотации Encoded
  • @Produces({MediaType.TEXT_HTML}) — определяет тип носителя, который может создать метод, в данном случае «text / html». Ответом будет HTML-документ со статусом 201, указывающий вызывающей стороне, что запрос был выполнен и привел к созданию нового ресурса.

отклик

  • в случае успеха: текстовый / html-документ с HTTP-статусом 201 Created и заголовком Location, указывающим, где был создан ресурс
  • по ошибке:
    • 400 Bad request если недостаточно данных
    • 409 Conflict если на стороне сервера определен подкаст с таким же фидом

3.2.2. Читать подкаст (ы)

3.2.2.1. дизайн

API поддерживает две операции чтения:

  • вернуть коллекцию подкастов
  • вернуть подкаст, идентифицированный по id
Описание URI HTTP метод
HTTP Status response
Вернуть все подкасты ? / Подкастов / orderByInsertionDate = {ASC | DESC} & numberDaysToLookBack = {} Вал ПОЛУЧИТЬ 200 ОК
Добавить новый подкаст (все значения должны быть отправлены) / подкасты / {ID} ПОЛУЧИТЬ 200 ОК

Обратите внимание на параметры запроса для ресурса коллекции — orderByInsertionDate и numberDaysToLookBack. Имеет смысл добавить фильтры в качестве параметров запроса в URI и не быть частью пути.

3.2.2.2. Реализация

3.2.2.2.1. Читать все подкасты («/»)

Читать все ресурсы

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
 * Returns all resources (podcasts) from the database
 *
 * @return
 * @throws IOException
 * @throws JsonMappingException
 * @throws JsonGenerationException
 * @throws AppException
 */
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public List<Podcast> getPodcasts(
        @QueryParam("orderByInsertionDate") String orderByInsertionDate,
        @QueryParam("numberDaysToLookBack") Integer numberDaysToLookBack)
        throws JsonGenerationException, JsonMappingException, IOException,
        AppException {
    List<Podcast> podcasts = podcastService.getPodcasts(
            orderByInsertionDate, numberDaysToLookBack);
    return podcasts;
}

Аннотации

  • @GET — указывает, что метод отвечает на запросы HTTP GET
  • @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) — определяет тип носителя), который может создавать метод, в данном случае либо "application/json" либо "application/xml" (вам нужен @XmlRootElement перед Podcast класс). Ответом будет список подкастов в формате JSON или XML.

отклик

  • список подкастов из базы данных и статус HTTP 200 OK

3.2.2.2.1. Читать один подкаст

Читать один ресурс по идентификатору

01
02
03
04
05
06
07
08
09
10
11
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getPodcastById(@PathParam("id") Long id)
        throws JsonGenerationException, JsonMappingException, IOException,
        AppException {
    Podcast podcastById = podcastService.getPodcastById(id);
    return Response.status(200).entity(podcastById)
            .header("Access-Control-Allow-Headers", "X-extra-header")
            .allow("OPTIONS").build();
}

Аннотации

  • @GET — указывает, что метод отвечает на запросы HTTP GET
  • @Path("{id}") — определяет путь URI, для которого метод класса будет обслуживать запросы. Значение «id» является встроенной переменной, создающей шаблон пути URI. Он используется в сочетании с переменной @PathParam .
    • @PathParam("id") — связывает значение параметра шаблона URI («id») с параметром метода ресурса. Значение является URL-декодированным, если только оно не отключено с @Encoded аннотации @Encoded . Значение по умолчанию можно указать с @DefaultValue аннотации @DefaultValue .
  • @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) — определяет тип носителя), который может создавать метод, в данном случае "application/json" или "application/xml" (вам необходим @XmlRootElement перед Podcast класс ).

отклик

  • в случае успеха: запрошенный подкаст с HTTP-статусом 200 OK . Формат может быть xml или JSON, в зависимости от значения Accept -header, отправленного клиентом (может ставить application / xml или application / json)
  • по ошибке: 404 Not found если подкаст с указанным идентификатором не существует в базе данных

3.2.3. Обновить подкаст

3.2.3.1. дизайн

Описание URI HTTP метод
HTTP Status response
Обновить подкаст ( полностью ) / подкасты / {ID} ПОЛОЖИТЬ 200 ОК
Обновление подкаста ( частично ) / подкасты / {ID} ПОЧТА 200 ОК

На арене REST вы будете делать два вида обновлений:

  1. полные обновления — вот где вы будете предоставлять все
  2. частичные обновления — когда только некоторые свойства будут отправлены по сети для обновления

Для полных обновлений довольно ясно, что вы можете использовать метод PUT, и вы соответствуете спецификации метода в RFC 2616 .

Теперь для частичного обновления есть куча предложений / дебатов о том, что использовать:

  1. через PUT
  2. через POST
  3. через патч

Позвольте мне рассказать, почему я считаю, что первый вариант (с PUT) — НЕТ GO. Ну и согласно спецификации

«Если Request-URI ссылается на уже существующий ресурс, вложенный объект СЛЕДУЕТ рассматривать как модифицированную версию, находящуюся на исходном сервере». [1]

если я хотел бы обновить только свойство заголовка подкаста с идентификатором 2

Команда PUT для частичного обновления

01
02
03
04
05
06
07
08
09
10
11
PUT http://localhost:8888/demo-rest-jersey-spring/podcasts/2 HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: application/json
Content-Length: 155
Host: localhost:8888
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
 
{
    "title":"New Title"
}

тогда, в соответствии со спецификацией, ресурс, «хранящийся» в местоположении, должен иметь только идентификатор и заголовок, и, очевидно, мои намерения были не в этом.

Второй вариант через POST… мы можем «злоупотребить» этим, и это именно то, что я сделал в реализации, но, похоже, он мне не подходит, потому что спецификация для POST гласит:

«Размещаемая сущность подчиняется этому URI так же, как файл подчиняется каталогу, в котором он находится, новостная статья подчиняется группе новостей, в которой она размещена, или запись подчиняется базе данных». [1 ]

Это не похоже на частичное обновление дела для меня …

Третий вариант — использовать PATCH, и я думаю, что это основная причина, по которой метод ожил:

«Несколько приложений, расширяющих протокол передачи гипертекста (HTTP)
требуется функция для частичной модификации ресурса. Существующий
Метод HTTP PUT позволяет только полную замену документа.
Это предложение добавляет новый метод HTTP, PATCH, чтобы изменить существующий
HTTP-ресурс. »[2]

Я почти уверен, что это будет использоваться в будущем для частичных обновлений, но, поскольку он еще не является частью спецификации и еще не реализован в Джерси, я решил использовать второй вариант с POST для этой демонстрации. Если вы действительно хотите реализовать частичное обновление в Java с помощью PATCH, ознакомьтесь с этой статьей — Поддержка прозрачного PATCH в JAX-RS 2.0

3.2.3.1. Реализация

3.2.3.1.1. Полное обновление

Создать или полностью обновить метод реализации ресурса

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@PUT
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response putPodcastById(@PathParam("id") Long id, Podcast podcast)
        throws AppException {
 
    Podcast podcastById = podcastService.verifyPodcastExistenceById(id);
 
    if (podcastById == null) {
        // resource not existent yet, and should be created under the
        // specified URI
        Long createPodcastId = podcastService.createPodcast(podcast);
        return Response
                .status(Response.Status.CREATED)
                // 201
                .entity("A new podcast has been created AT THE LOCATION you specified")
                .header("Location",
                        "http://localhost:8888/demo-rest-jersey-spring/podcasts/"
                                + String.valueOf(createPodcastId)).build();
    } else {
        // resource is existent and a full update should occur
        podcastService.updateFullyPodcast(podcast);
        return Response
                .status(Response.Status.OK)
                // 200
                .entity("The podcast you specified has been fully updated created AT THE LOCATION you specified")
                .header("Location",
                        "http://localhost:8888/demo-rest-jersey-spring/podcasts/"
                                + String.valueOf(id)).build();
    }
}

Аннотации

  • @PUT — указывает, что метод отвечает на запросы HTTP PUT
  • @Path("{id}") — определяет путь URI, для которого метод класса будет обслуживать запросы. Значение «id» является встроенной переменной, создающей шаблон пути URI. Он используется в сочетании с переменной @PathParam .
    • @PathParam("id") — связывает значение параметра шаблона URI («id») с параметром метода ресурса. Значение является URL-декодированным, если только оно не отключено с @Encoded аннотации @Encoded . Значение по умолчанию можно указать с @DefaultValue аннотации @DefaultValue .
  • @Consumes({MediaType.APPLICATION_JSON}) — определяет тип мультимедиа, который принимает метод, в данном случае "application/json"
  • @Produces({MediaType.TEXT_HTML}) — определяет тип носителя), который может создать метод, в данном случае «text / html».

будет HTML-документ, содержащий различные сообщения и статус в зависимости от того, какое действие было предпринято

Respsonse

  • на создание
    • в случае успеха: 201 Created и в заголовке Location указывается место, где был создан ресурс
    • по ошибке: 400 Bad request если для вставки не указаны минимально необходимые свойства
  • при полном обновлении
    • в случае успеха: 200 OK
    • по ошибке: 400 Bad Request если не все свойства предоставлены

3.2.3.1.2. Частичное обновление

Частичное обновление

01
02
03
04
05
06
07
08
09
10
11
12
//PARTIAL update
@POST
@Path("{id}")  
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response partialUpdatePodcast(@PathParam("id") Long id, Podcast podcast) throws AppException {
    podcast.setId(id);
    podcastService.updatePartiallyPodcast(podcast);
    return Response.status(Response.Status.OK)// 200
            .entity("The podcast you specified has been successfully updated")
            .build();  
}

Аннотации

  • @POST — указывает, что метод отвечает на запросы HTTP POST
  • @Path("{id}") — определяет путь URI, для которого метод класса будет обслуживать запросы. Значение «id» является встроенной переменной, создающей шаблон пути URI. Он используется в сочетании с переменной @PathParam .
    • @PathParam("id") — связывает значение параметра шаблона URI («id») с параметром метода ресурса. Значение является URL-декодированным, если только оно не отключено с @Encoded аннотации @Encoded . Значение по умолчанию можно указать с @DefaultValue аннотации @DefaultValue .
  • @Consumes({MediaType.APPLICATION_JSON}) — определяет тип мультимедиа, который принимает метод, в данном случае "application/json"
  • @Produces({MediaType.TEXT_HTML}) — определяет тип носителя), который может создать метод, в данном случае "text/html" .

отклик

  • в случае успеха: 200 OK
  • по ошибке: 404 Not Found , если в указанном месте больше нет доступных ресурсов

3.2.4. Удалить подкаст

3.2.4.1. дизайн

Описание URI HTTP метод
HTTP Status response
Удаляет все подкасты / Подкасты / УДАЛИТЬ 204 Нет контента
Удаляет подкаст в указанном месте / подкасты / {ID} УДАЛИТЬ 204 Нет контента

3.2.4.2. Реализация

3.2.4.2.1. Удалить все ресурсы

Удалить все ресурсы

1
2
3
4
5
6
7
@DELETE
@Produces({ MediaType.TEXT_HTML })
public Response deletePodcasts() {
    podcastService.deletePodcasts();
    return Response.status(Response.Status.NO_CONTENT)// 204
            .entity("All podcasts have been successfully removed").build();
}

Аннотации

  • @DELETE — указывает, что метод отвечает на запросы HTTP DELETE.
  • @Produces({MediaType.TEXT_HTML}) — определяет тип носителя, который может создать метод, в данном случае «text / html».

отклик

  • Ответом будет html-документ со статусом 204 «Нет содержимого», указывающий вызывающей стороне, что запрос был выполнен.

3.2.4.2.2. Удалить один ресурс

Удалить один ресурс

1
2
3
4
5
6
7
8
@DELETE
@Path("{id}")
@Produces({ MediaType.TEXT_HTML })
public Response deletePodcastById(@PathParam("id") Long id) {
    podcastService.deletePodcastById(id);
    return Response.status(Response.Status.NO_CONTENT)// 204
            .entity("Podcast successfully removed from database").build();
}

Аннотации

  • @DELETE — указывает, что метод отвечает на запросы HTTP DELETE.
  • @Path("{id}") — определяет путь URI, для которого метод класса будет обслуживать запросы. Значение «id» является встроенной переменной, создающей шаблон пути URI. Он используется в сочетании с переменной @PathParam .
    • @PathParam("id") — связывает значение параметра шаблона URI («id») с параметром метода ресурса. Значение является URL-декодированным, если только оно не отключено с @Encoded аннотации @Encoded . Значение по умолчанию можно указать с @DefaultValue аннотации @DefaultValue .
  • @Produces({MediaType.TEXT_HTML}) — определяет тип носителя, который может создать метод, в данном случае «text / html».

отклик

  • в случае успеха: при удалении подкаста возвращается статус успешного завершения 204 No Content
  • из-за ошибки: подкаст больше недоступен и возвращается статус 404 Not found

4. Регистрация

Путь каждого запроса и сущность ответа будут регистрироваться, когда уровень ведения журнала установлен на DEBUG. Это разработано как обертка, функциональность стиля AOP с помощью фильтров Jetty.

См. Мой пост Как войти в Spring с SLF4J и Logback для более подробной информации по этому вопросу.

5. Обработка исключений

В случае ошибок я решил ответить унифицированной структурой сообщений об ошибках. Вот пример того, как может выглядеть ответ об ошибке:

Пример — ответ на сообщение об ошибке

1
2
3
4
5
6
7
{
   "status": 400,
   "code": 400,
   "message": "Provided data not sufficient for insertion",
   "developerMessage": "Please verify that the feed is properly generated/set"
}

Примечание: следите за обновлениями, потому что в следующем посте будет представлена ​​более подробная информация об обработке ошибок в REST с Джерси.

6. Добавьте поддержку CORS на стороне сервера.

Я расширил возможности API, разработанного для данного руководства, для поддержки перекрестного совместного использования ресурсов (CORS) на стороне сервера.

Пожалуйста, смотрите мой пост Как добавить поддержку CORS на стороне сервера в Java с Джерси для более подробной информации по этому вопросу.

7. Тестирование

7.1. Интеграционные тесты в Java

Чтобы протестировать приложение, я буду использовать Jersey Client и выполнять запросы к работающему серверу Jetty с развернутым на нем приложением. Для этого я буду использовать Maven Failsafe Plugin .

7.1.1. конфигурация

7.1.1.1 Зависимость клиента Джерси

Для создания клиента jersey-client требуется jar jersey-client в classpath. С Maven вы можете добавить его в качестве зависимости к файлу pom.xml :

Джерси Клиент maven зависимость

1
2
3
4
5
6
<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>${jersey.version}</version>
    <scope>test</scope>
</dependency>

7.1.1.2. Отказоустойчивый плагин

Отказоустойчивыми Плагин используется во время интеграционного тестирования и проверки этапов жизненного цикла сборки для выполнения интеграционных тестов приложения. Плагин Failsafe не завершит сборку на этапе тестирования интеграции, что позволит выполнить этап после тестирования интеграции.
Чтобы использовать Failsafe Plugin, вам необходимо добавить следующую конфигурацию в вашpom.xml

Конфигурация Maven Failsafe Plugin

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugins>
    [...]
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.16</version>
        <executions>
            <execution>
                <id>integration-test</id>
                <goals>
                    <goal>integration-test</goal>
                </goals>
            </execution>
            <execution>
                <id>verify</id>
                <goals>
                    <goal>verify</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    [...]
</plugins>

7.1.1.2. Jetty Maven Плагин

Интеграционные тесты будут выполняться на работающем сервере Jetty, который будет запущен только для выполнения тестов. Для этого вам необходимо настроить следующее выполнение в jetty-maven-plugin:

Конфигурация Jetty Maven Plugin для интеграционных тестов

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<plugins>
    <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>${jetty.version}</version>
        <configuration>
            <jettyConfig>${project.basedir}/src/main/resources/config/jetty9.xml</jettyConfig>
            <stopKey>STOP</stopKey>
            <stopPort>9999</stopPort>
            <stopWait>5</stopWait>
            <scanIntervalSeconds>5</scanIntervalSeconds>
        [...]
        </configuration>
        <executions>
            <execution>
                <id>start-jetty</id>
                <phase>pre-integration-test</phase>
                <goals>
                    <!-- stop any previous instance to free up the port -->
                    <goal>stop</goal>              
                    <goal>run-exploded</goal>
                </goals>
                <configuration>
                    <scanIntervalSeconds>0</scanIntervalSeconds>
                    <daemon>true</daemon>
                </configuration>
            </execution>
            <execution>
                <id>stop-jetty</id>
                <phase>post-integration-test</phase>
                <goals>
                    <goal>stop</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    [...]
</plugins>

Примечание. На этом pre-integration-testэтапе сервер Jetty будет запущен после остановки любого работающего экземпляра, чтобы освободить порт, и в этом случае post-integration-phaseон будет остановлен. Значение scanIntervalSecondsдолжно быть установлено на 0 и равно daemontrue.

Предупреждение кода: найдите полный файл pom.xml на GitHub

7.1.2. Постройте интеграционные тесты

Я использую JUnit в качестве основы тестирования. По умолчанию подключаемый модуль Failsafe автоматически включает все тестовые классы со следующими шаблонами подстановочных знаков:

  • "**/IT*.java" — включает в себя все его подкаталоги и все имена файлов Java, которые начинаются с «IT».
  • "**/*IT.java" — включает все его подкаталоги и все имена файлов Java, которые заканчиваются на «IT».
  • "**/*ITCase.java" — включает все его подкаталоги и все имена файлов Java, которые заканчиваются на «ITCase».

Я создал один тестовый класс — RestDemoServiceITкоторый будет тестировать методы чтения (GET), но процедура должна быть одинаковой для всех остальных:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RestDemoServiceIT {
 
    [....]
    @Test
    public void testGetPodcast() throws JsonGenerationException,
            JsonMappingException, IOException {
 
        ClientConfig clientConfig = new ClientConfig();
        clientConfig.register(JacksonFeature.class);
 
        Client client = ClientBuilder.newClient(clientConfig);
 
        WebTarget webTarget = client
                .target("http://localhost:8888/demo-rest-jersey-spring/podcasts/2");
 
        Builder request = webTarget.request(MediaType.APPLICATION_JSON);
 
        Response response = request.get();
        Assert.assertTrue(response.getStatus() == 200);
 
        Podcast podcast = response.readEntity(Podcast.class);
 
        ObjectMapper mapper = new ObjectMapper();
        System.out
                .print("Received podcast from database *************************** "
                        + mapper.writerWithDefaultPrettyPrinter()
                                .writeValueAsString(podcast));
 
    }
}

Замечания:

  • Мне также пришлось зарегистрировать JacksonFeature для клиента, чтобы я мог маршалировать ответ подкаста в формате JSON — response.readEntity (Podcast.class)
  • Я тестирую на работающей Jetty на порту 8888 — в следующем разделе я покажу вам, как запустить Jetty на желаемом порту
  • Я ожидаю 200 статус для моего запроса
  • С помощью org.codehaus.jackson.map.ObjectMapperя показываю ответ JSON довольно отформатирован

7.1.3. Запуск интеграционных тестов

Плагин Failsafe может быть вызван путем вызова verifyфазы жизненного цикла сборки.

Команда Maven для вызова интеграционных тестов

1
mvn verify

Чтобы запустить джетти через порт 8888, вам нужно установить jetty.portсвойство 8888. В Eclipse я использую следующую конфигурацию:

Запустите интеграционные тесты из Eclipse

Запустите интеграционные тесты из Eclipse

7.2. Интеграционные тесты с SoapUI

Недавно я заново открыл SoapUI после интенсивного его использования для тестирования веб-сервисов на основе SOAP. В последних версиях (на момент написания статьи последняя версия 5.0.0) он предлагает довольно хорошую функциональность для тестирования веб-сервисов на основе REST, и в следующих версиях это должно улучшиться. Поэтому, если вы не разрабатываете собственную инфраструктуру / инфраструктуру для тестирования сервисов REST, почему бы не попробовать SoapUI. До сих пор я был доволен результатами и решил создать видеоурок, который вы теперь можете найти на YouTube на нашем канале:

8. Управление версиями

Есть три основных варианта

  1. URL : «/ v1 / podcasts / {id}»
  2. Accept / Content-type header : application / json; версия = 1

Потому что я разработчик, а не RESTafarian, но я бы сделал вариант URL. Все, что мне нужно было бы сделать на стороне реализации для этого примера, это изменить @Pathаннотацию значения в PodcastResourceклассе с

Управление версиями в пути

1
2
3
@Component
@Path("/v1/podcasts")
public class PodcastResource {...}

Конечно, в производственном приложении вы бы не хотели, чтобы каждый класс ресурсов был предварительно снабжен префиксом с номером версии, вы бы хотели, чтобы версия каким-то образом обрабатывалась через фильтр AOP-способом. Может быть, что-то подобное будет в следующем посте …

Вот несколько замечательных ресурсов от людей, которые лучше понимают ситуацию:

9. Резюме

Ну вот и все. Я должен поздравить вас, если вы зашли так далеко, но я надеюсь, что вы могли бы кое-что узнать из этого урока о REST, например, о разработке REST API, реализации REST API в Java, тестировании REST API и многом другом. Если бы вы это сделали, я был бы очень признателен, если бы вы помогли ему распространиться, оставив комментарий или поделившись им в Twitter, Google+ или Facebook. Спасибо!Не забудьте также проверить Podcastpedia.org — вы обязательно найдете интересные подкасты и эпизоды. Мы благодарны за вашу поддержку.

Если вам понравилась эта статья, мы будем очень признательны за небольшой вклад в нашу работу! Пожертвуйте сейчас с Paypal.

10. Ресурсы

10.1. Исходный код

10,2. Веб-ресурсы

  1. HTTP — протокол передачи гипертекста — HTTP / 1.1 — RFC2616
  2. rfc5789 — метод PATCH для HTTP
  3. Джерси Руководство пользователя
  4. Определения кода состояния HTTP
  5. ОТДЫХ — http://en.wikipedia.org/wiki/Representational_State_Transfer
  6. CRUD — http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
  7. Java API для сервисов RESTful (JAX-RS)
  8. Jersey — RESTful веб-сервисы на Java
  9. HTTP PUT, PATCH или POST — частичные обновления или полная замена?
  10. Поддержка прозрачного PATCH в JAX-RS 2.0
  11. Maven Failsafe Плагин
  12. Maven Failsafe Plugin Использование
  13. SoapUI 5.0 выпущен сегодня!
  14. SoapUI — Использование утверждений скрипта
  15. [Видео] REST + JSON API Design — лучшие практики для разработчиков
  16. [Видео] RESTful API Design — второе издание
  17. Закон Деметры

10.3. Ресурсы, связанные с Codingpedia