Статьи

Предоставление HTTP Restful API с входящими адаптерами.

1. Введение

Цель этого поста — реализовать HTTP Restful API с помощью входящих адаптеров Spring Integration HTTP . Этот урок разделен на две части:

  • Пример конфигурации XML (этот же пост).
  • Пример Java DSL. Это будет объяснено в следующей части этого руководства, где показано, как настроить приложение с помощью Spring Integration Java DSL , с примерами для Java 7 и Java 8.

Прежде чем смотреть на код, давайте взглянем на следующую диаграмму, которая показывает различные сервисы, предоставляемые приложением:

диаграмма

Операции GET обрабатываются входящим шлюзом HTTP, а остальные (PUT, POST и DELETE) обрабатываются адаптерами входящего канала HTTP, так как тело ответа не отправляется обратно клиенту. Каждая операция будет объяснена в следующих разделах:

  1. Вступление
  2. Конфигурация приложения
  3. Получить операцию
  4. Положите и почтовые операции
  5. Удалить операцию
  6. Вывод

Исходный код доступен на Github .

2. Конфигурация приложения

Файл web.xml содержит определение сервлета диспетчера:

01
02
03
04
05
06
07
08
09
10
11
12
<servlet>
    <servlet-name>springServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:xpadro/spring/integration/configuration/http-inbound-config.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>springServlet</servlet-name>
    <url-pattern>/spring/*</url-pattern>
</servlet-mapping>

Файл http-inbound-config.xml будет объяснен в следующих разделах.

Файл pom.xml подробно описан ниже. Важно отметить библиотеки Джексона. Поскольку мы будем использовать JSON для представления наших ресурсов, эти библиотеки должны присутствовать в пути к классам. В противном случае фреймворк не зарегистрирует необходимый конвертер.

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
<properties>
    <spring-version>4.1.3.RELEASE</spring-version>
    <spring-integration-version>4.1.0.RELEASE</spring-integration-version>
    <slf4j-version>1.7.5</slf4j-version>
    <junit-version>4.9</junit-version>
    <jackson-version>2.3.0</jackson-version>
</properties>
 
<dependencies>
    <!-- Spring Framework - Core -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring-version}</version>
    </dependency>
     
    <!-- Spring Framework - Integration -->
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-core</artifactId>
        <version>${spring-integration-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-http</artifactId>
        <version>${spring-integration-version}</version>
    </dependency>
     
    <!-- JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson-version}</version>
    </dependency>
     
    <!-- Testing -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit-version}</version>
        <scope>test</scope>
    </dependency>
     
    <!-- Logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j-version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j-version}</version>
    </dependency>
</dependencies>

3. Получить операцию

Конфигурация потока показана ниже:

HTTP-въездной-config.xml

Шлюз получает запросы по этому пути: / people / {personId}. После получения запроса создается сообщение и отправляется на канал httpGetChannel. Затем шлюз будет ждать, пока активатор службы (personEndpoint) вернет ответ:

1
2
3
4
5
6
<int-http:inbound-gateway request-channel="httpGetChannel" reply-channel="responseChannel" supported-methods="GET" path="/persons/{personId}" payload-expression="#pathVariables.personId">
     
    <int-http:request-mapping consumes="application/json" produces="application/json">
</int-http:request-mapping></int-http:inbound-gateway>
 
<int:service-activator ref="personEndpoint" method="get" input-channel="httpGetChannel" output-channel="responseChannel"></int:service-activator>

Теперь необходимо пояснить некоторые моменты:

  • поддерживаемые методы : этот атрибут указывает, какие методы поддерживаются шлюзом (только запросы GET).
  • выражение полезной нагрузки : здесь мы получаем значение из переменной personId в шаблоне URI и помещаем его в полезную нагрузку сообщения. Например, путь запроса «/ people / 3» станет сообщением со значением «3» в качестве полезной нагрузки.
  • Отображение запроса : мы можем включить этот элемент, чтобы указать несколько атрибутов и отфильтровать, какие запросы будут отображаться на шлюз. В этом примере этот шлюз будет обрабатывать только запросы, которые содержат значение application / json для заголовка Content-Type (атрибут потребляет) и заголовок Accept (производит атрибут).

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

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
@Component
public class PersonEndpoint {
    private static final String STATUSCODE_HEADER = "http_statusCode";
     
    @Autowired
    private PersonService service;
     
    public Message<?> get(Message<String> msg) {
        long id = Long.valueOf(msg.getPayload());
        ServerPerson person = service.getPerson(id);
         
        if (person == null) {
            return MessageBuilder.fromMessage(msg)
                .copyHeadersIfAbsent(msg.getHeaders())
                .setHeader(STATUSCODE_HEADER, HttpStatus.NOT_FOUND)
                .build();
        }
         
        return MessageBuilder.withPayload(person)
            .copyHeadersIfAbsent(msg.getHeaders())
            .setHeader(STATUSCODE_HEADER, HttpStatus.OK)
            .build();
    }
     
    //Other operations
}

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

Сейчас мы проверим, что все работает как положено. Сначала мы определяем класс ClientPerson, в который будет преобразован ответ:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientPerson implements Serializable {
    private static final long serialVersionUID = 1L;
     
    @JsonProperty("id")
    private int myId;
    private String name;
     
    public ClientPerson() {}
     
    public ClientPerson(int id, String name) {
        this.myId = id;
        this.name = name;
    }
     
    //Getters and setters
}

Затем мы реализуем тест. В методе buildHeaders мы указываем заголовки Accept и Content-Type. Помните, что мы ограничивали запросы значениями ‘application / json’ в этих заголовках.

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
@RunWith(BlockJUnit4ClassRunner.class)
public class GetOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
     
    private HttpHeaders buildHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON);
         
        return headers;
    }
     
    @Test
    public void getResource_responseIsConvertedToPerson() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
        assertEquals("John" , response.getBody().getName());
        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
     
    @Test
    public void getResource_responseIsReceivedAsJson() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<String> response = restTemplate.exchange(URL, HttpMethod.GET, entity, String.class, 1);
        assertEquals("{\"id\":1,\"name\":\"John\",\"age\":25}", response.getBody());
        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
     
    @Test(expected=HttpClientErrorException.class)
    public void getResource_sendXml_415errorReturned() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_XML);
        HttpEntity<Integer> entity = new HttpEntity<>(headers);
        restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
    }
     
    @Test(expected=HttpClientErrorException.class)
    public void getResource_expectXml_receiveJson_406errorReturned() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_XML));
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<Integer> entity = new HttpEntity<>(headers);
        restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
    }
     
    @Test(expected=HttpClientErrorException.class)
    public void getResource_resourceNotFound_404errorReturned() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 8);
    }
}

Если не указать правильное значение в заголовке Content-Type, возникнет ошибка 415 Unsupported Media Type, поскольку шлюз не поддерживает этот тип носителя.

С другой стороны, указание неверного значения в заголовке Accept приведет к ошибке 406 Not Acceptable, поскольку шлюз возвращает контент другого типа, чем ожидалось.

4. Положите и пост операции

Для операций PUT и POST мы используем один и тот же адаптер входящего канала HTTP, используя возможность определения нескольких путей и методов к нему. Как только запрос поступит, маршрутизатор будет отвечать за доставку сообщения к правильной конечной точке.

HTTP-въездной-config.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<int-http:inbound-channel-adapter channel="routeRequest"
    status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"
    supported-methods="POST, PUT"
    path="/persons, /persons/{personId}"
    request-payload-type="xpadro.spring.integration.server.model.ServerPerson">
     
    <int-http:request-mapping consumes="application/json"/>
</int-http:inbound-channel-adapter>
 
<int:router input-channel="routeRequest" expression="headers.http_requestMethod">
    <int:mapping value="PUT" channel="httpPutChannel"/>
    <int:mapping value="POST" channel="httpPostChannel"/>
</int:router>
 
<int:service-activator ref="personEndpoint" method="put" input-channel="httpPutChannel"/>
<int:service-activator ref="personEndpoint" method="post" input-channel="httpPostChannel"/>

Этот адаптер канала включает два новых атрибута:

  • выражение кода состояния : по умолчанию адаптер канала подтверждает получение запроса и возвращает код состояния 200. Если мы хотим переопределить это поведение, мы можем указать другой код состояния в этом атрибуте. Здесь мы указываем, что эти операции будут возвращать код состояния 204 Нет содержимого.
  • request-payload-type : Этот атрибут указывает, в какой класс будет преобразовано тело запроса. Если мы не определим его, он не сможет преобразовать в класс, который ожидает активатор службы (ServerPerson).

Когда запрос получен, адаптер отправляет его на канал routeRequest, где его ожидает маршрутизатор. Этот маршрутизатор будет проверять заголовки сообщений и, в зависимости от значения заголовка http_requestMethod, доставит его в соответствующую конечную точку.

Обе операции PUT и POST обрабатываются одним и тем же компонентом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Component
public class PersonEndpoint {
    @Autowired
    private PersonService service;
     
    //Get operation
     
    public void put(Message<ServerPerson> msg) {
        service.updatePerson(msg.getPayload());
    }
     
    public void post(Message<ServerPerson> msg) {
        service.insertPerson(msg.getPayload());
    }
}

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

PutOperationsTest проверяет, что возвращен правильный код состояния и что ресурс обновлен:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(BlockJUnit4ClassRunner.class)
public class PutOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
     
    //build headers method
     
    @Test
    public void updateResource_noContentStatusCodeReturned() {
        HttpEntity<Integer> getEntity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.GET, getEntity, ClientPerson.class, 4);
        ClientPerson person = response.getBody();
        person.setName("Sandra");
        HttpEntity<ClientPerson> putEntity = new HttpEntity<ClientPerson>(person, buildHeaders());
         
        response = restTemplate.exchange(URL, HttpMethod.PUT, putEntity, ClientPerson.class, 4);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
         
        response = restTemplate.exchange(URL, HttpMethod.GET, getEntity, ClientPerson.class, 4);
        person = response.getBody();
        assertEquals("Sandra", person.getName());
    }
}

PostOperationsTest проверяет, что новый ресурс был добавлен:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(BlockJUnit4ClassRunner.class)
public class PostOperationsTest {
    private static final String POST_URL = "http://localhost:8081/int-http-xml/spring/persons";
    private static final String GET_URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
     
    //build headers method
     
    @Test
    public void addResource_noContentStatusCodeReturned() {
        ClientPerson person = new ClientPerson(9, "Jana");
        HttpEntity<ClientPerson> entity = new HttpEntity<ClientPerson>(person, buildHeaders());
         
        ResponseEntity<ClientPerson> response = restTemplate.exchange(POST_URL, HttpMethod.POST, entity, ClientPerson.class);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
         
        HttpEntity<Integer> getEntity = new HttpEntity<>(buildHeaders());
        response = restTemplate.exchange(GET_URL, HttpMethod.GET, getEntity, ClientPerson.class, 9);
        person = response.getBody();
        assertEquals("Jana", person.getName());
    }
}

5. Удалить операцию

Последняя операция нашего restful API — это операция удаления. На этот раз мы используем одноканальный адаптер для этой цели:

01
02
03
04
05
06
07
08
09
10
<int-http:inbound-channel-adapter channel="httpDeleteChannel"
    status-code-expression="T(org.springframework.http.HttpStatus).NO_CONTENT"
    supported-methods="DELETE"
    path="/persons/{personId}"
    payload-expression="#pathVariables.personId">
     
    <int-http:request-mapping consumes="application/json"/>
</int-http:inbound-channel-adapter>
 
<int:service-activator ref="personEndpoint" method="delete" input-channel="httpDeleteChannel"/>

Адаптер канала позволяет нам определить возвращаемый код состояния, и мы используем атрибут payload-expression для сопоставления запрошенного personId с телом сообщения. Конфигурация немного отличается от конфигурации в предыдущих операциях, но здесь ничего не объяснено.

Активатор службы, наша конечная точка, попросит службу поддержки удалить этот ресурс.

1
2
3
4
public void delete(Message<String> msg) {
    long id = Long.valueOf(msg.getPayload());
    service.deletePerson(id);
}

Наконец, обязательный тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(BlockJUnit4ClassRunner.class)
public class DeleteOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-xml/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
     
    //build headers method
     
    @Test
    public void deleteResource_noContentStatusCodeReturned() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.DELETE, entity, ClientPerson.class, 3);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
         
        try {
            response = restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 3);
            Assert.fail("404 error expected");
        } catch (HttpClientErrorException e) {
            assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
        }
    }
}

6. Заключение

Этот пост был введением в наше приложение, чтобы понять, как оно структурировано с известной точки зрения (конфигурация xml). В следующей части этого руководства мы собираемся реализовать это же приложение с использованием Java DSL. Приложение будет настроено для работы с Java 8, но когда будут использованы лямбды, я также покажу, как это можно сделать с Java 7.

Я публикую свои новые сообщения в Google Plus и Twitter. Следуйте за мной, если вы хотите быть в курсе нового контента.