Spring in Action Covers Spring 5-1--11 — страница 29 из 63

Листинг 6.1. Angular компонент для отображения последних тако

import { Component, OnInit, Injectable } from '@angular/core';

import { Http } from '@angular/http';

import { HttpClient } from '@angular/common/http';

@Component({

 selector: 'recent-tacos',

 templateUrl: 'recents.component.html',

 styleUrls: ['./recents.component.css']

})

 @Injectable()

 export class RecentTacosComponent implements OnInit {

   recentTacos: any;

   constructor(private httpClient: HttpClient) { }

   ngOnInit() {

     this.httpClient.get('http://localhost:8080/design/recent') //Получает последние тако с сервера

       .subscribe(data => this.recentTacos = data);

 }

}

Обратите ваше внимание на метод ngOnInit(). В этом методе RecentTacosComponent использует внедренный модуль Http для выполнения HTTP-запроса GET к http://localhost:8080/design/latest, ожидая, что ответ будет содержать список дизайнов тако, который будет помещен в переменную модели recentTacos. Визуализация ( recents.component.html) представит данные этой модели в виде HTML, которые будут отображаться в браузере. Конечный результат может выглядеть примерно так, как показано на рисунке 6.2, после создания трех тако.

Рисунок 6.2 Отображение последних созданных тако

Недостающий фрагмент этой головоломки - это конечная точка (endpoint), которая обрабатывает запросы GET для /design/recent и отдает списком недавно созданных тако. Вы создадите новый контроллер для обработки такого запроса. Следующий листинг показывает этот контроллер.

Листинг 6.2. Контроллер RESTful для API запросов дизайнов тако

package tacos.web.api;

import java.util.Optional;

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

import org.springframework.data.domain.PageRequest;

import org.springframework.data.domain.Sort;

import org.springframework.hateoas.EntityLinks;

import org.springframework.http.HttpStatus;

import org.springframework.web.bind.annotation.CrossOrigin;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseStatus;

import org.springframework.web.bind.annotation.RestController;

import tacos.Taco;

import tacos.data.TacoRepository;

@RestController

@RequestMapping(path="/design", produces="application/json") //Обрабатывает запросы на /design

@CrossOrigin(origins="*") //Позволяет перекрестные запросы

public class DesignTacoController {

 private TacoRepository tacoRepo;

@Autowired

 EntityLinks entityLinks;

 public DesignTacoController(TacoRepository tacoRepo) {

   this.tacoRepo = tacoRepo;

 }

 @GetMapping("/recent")

 public Iterable recentTacos() { //Формирует и отдает последние дизайны тако

   PageRequest page = PageRequest.of(

     0, 12, Sort.by("createdAt").descending());

   return tacoRepo.findAll(page).getContent();

 }

}

Аннотация @RestController служит двум целям. Во-первых, это аннотация стереотипа, такая как @Controller и @Service, которая отмечает класс для обнаружения при сканировании компонентов. Но наиболее релевантным для обсуждения REST, аннотация @RestController сообщает Spring, что все методы-обработчики в контроллере должны иметь свое возвращаемое значение, записанное непосредственно в тело ответа, а не записываться в модель в представление для визуализации.

В качестве альтернативы, вы могли бы аннотировать DesignTacoController с помощью @Controller, как и с любым контроллером Spring MVC. Но тогда вам также нужно аннотировать все методы-обработчики с помощью @ResponseBody для достижения того же результата. Еще один вариант - вернуть объект ResponseEntity, о котором мы поговорим чуть позже.

Аннотация @RequestMapping на уровне класса работает с аннотацией @GetMapping в методе recentTacos(), чтобы указать, что метод recentTacos() отвечает за обработку запросов GET для /design/recent (именно это необходимо для вашего кода Angular).

Вы заметите, что аннотация @RequestMapping также устанавливает атрибут produces. Это указывает, что любой из методов-обработчиков в DesignTacoController будет обрабатывать запросы только в том случае, если заголовок Accept запроса включает в себя «application/json». Это не только ограничивает ваш API только выдачей результатов в виде JSON, но также позволяет другому контроллеру (возможно, DesignTacoController из главы 2) обрабатывать запросы с одинаковыми путями, если эти запросы не имеют требований к вывода в формате JSON. Несмотря на то, что это ограничивает ваш API-интерфейс только JSON (что подходит для ваших нужд), вы можете установить produces как массив String нескольких типов контента. Например, чтобы разрешить вывод XML, вы можете добавить” text/html " в атрибут produces:

@RequestMapping(path="/design", produces={"application/json", "text/xml"})

То что еще вы, возможно, заметили в листинге 6.2, это то, что класс аннотирован @CrossOrigin. Поскольку Angular-часть приложения будет работать на отдельном хосте и/или порту по API (по крайней мере, на данный момент), веб-браузер не позволит вашему Angular-клиенту использовать API. Это ограничение можно обойти, включив заголовки CORS (Cross-Origin Resource Sharing) в ответы сервера. Spring упрощает применение CORS с аннотацией @CrossOrigin. В данном случае @CrossOrigin позволяет клиентам из любого домена использовать API.

Логика в методе recentTacos() довольно проста. Он создает объект PageRequest, который указывает, что вы хотите получить первую (0-я) страницу с первыми 12 результатами сортировки в порядке убывания по дате создания тако. Короче говоря, вы хотите дюжину самых последних созданных дизайнов тако. PageRequest передается в вызов метода findAll() объекта TacoRepository,  и содержимое этой страницы результатов возвращается клиенту (ответ, как вы видели в листинге 6.1, будет использоваться в качестве данных модели для отображения пользователю).

Теперь предположим, что вы хотите создать endpoint, который извлекает один тако по его идентификатору. Используя переменную-заполнитель в пути метода обработчика и принимая переменную path, вы можете получить ID и использовать его для поиска объекта Taco используя репозиторий:

@GetMapping("/{id}")

public Taco tacoById(@PathVariable("id") Long id) {

 Optional optTaco = tacoRepo.findById(id);

   if (optTaco.isPresent()) {

     return optTaco.get();

 }

 return null;

}

Поскольку базовый (корневой) путь контроллера - /design, этот метод контроллера обрабатывает запросы GET для /design/{id}, где часть пути {id} является заполнителем. Фактическое значение в запросе задается параметру id, который сопоставляется с заполнителем {id} с помощью @PathVariable.

Внутри tacoById () параметр id передается методу findById() в репозиторий для получения Taco. findById() возвращает Optional, потому что возможна ситуация, что нет тако с переданным ID. Поэтому перед тем как получить результат необходимо проверить, существует ли тако по переданному идентификатору. Если такой тако существует, вы вызываете метод get() для Optional, чтобы получить Taco.

Если идентификатор не соответствует ни одному известному tacos, вы возвращаете null. Однако это далеко не идеально. Возвращая значение null, клиент получает ответ с пустым телом и кодом состояния HTTP 200 (OK). Клиент получает ответ, который он не может использовать, но код состояния указывает, что все в порядке. Лучшим подходом было бы вернуть ответ со статусом HTTP 404 (не найден).

Текущая реализация не имеет простого способа, чтобы возвращать код статуса 404 от tacoById(). Но если произвести небольшие изменения, это станет возможным:

@GetMapping("/{id}")

public ResponseEntity tacoById(@PathVariable("id") Long id) {

 Optional optTaco = tacoRepo.findById(id);

 if (optTaco.isPresent()) {

   return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);

 }

 return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);

}

Теперь вместо возврата объекта Taco функция tacoById () возвращает ResponseEntity . Если тако найдено, вы оборачиваете объект Taco в ResponseEntity с HTTP-статусом OK (что и было ранее). Но если тако не найдено, вы добавляете в ResponseEntity значение null вместе с HTTP-статусом NOT FOUND, чтобы указать, что клиент пытается получить тако, которого не существует.

Начало API Taco Cloud для вашего  Angular-овского (или любого другого, по вашим предпочтениям) клиента положено. Для тестирования разработки вы также можете использовать утилиты командной строки, такие как curl или HTTPie (https://httpie.org/) чтобы лучше понимать API. Например, следующая командная строка показывает, как вы могли бы получить недавно созданные тако с curl:

$ curl localhost:8080/design/recent

Или такой командой, если вы предпочитаете HTTPie:

$ http :8080/design/recent

Но реализация endpoint, которая возвращает информацию, - это только начало. Что делать, если ваш API должен получать данные от клиента? Давайте посмотрим, как вы можете написать методы контроллера, которые обрабатывают входные данные запросов.

6.1.2 Отправка данных на сервер

Сейчас приложение может вернуть дюжину самых последних созданных тако. Но как эти тако были созданы изначально?

Вы еще не удалили ни один код из главы 2, поэтому у вас все еще есть оригинальный DesignTacoController, который отображает форму дизайна Taco и обрабатывает отправку формы. Это отличный способ получить некоторые тестовые данные для тестирования созданного вами API. Но если вы собираетесь преобразовать Taco Cloud в одностраничное приложение, вам нужно будет создать компоненты Angular и соответствующие endpoint-ы, чтобы заменить форму дизайна Taco из главы 2.