Контроллер
В этой и следующей главах мы будем разрабатывать простейший сайт для работы со списком задач.
Исходники этого проекта можно скачать здесь.
Rest
Для общения с сервером необходимо отправлять ему данные и обрабатывать их ответы. То, как устроены посылки, называется стандартом.
Хотя при общении с сервером и встречаются очень разные задачи, всё равно можно выделить некоторые общие принципы. Такие принципы сформулированы в архитектуре REST. REST расшифровывается как Representational State Transfer - передача состояния представления.
Напомню: представление в концепции MVC(Model View Controller) - это View.
Вместо того чтобы рисовать интерфейс сайта за счёт ресурсов сервера, он
называется backend - бэкенд, мы создаём универсальный
способ обращения к серверу, который просто выдаёт те или иные данные в формате json
.
При таком подходе внешнюю оболочку сайта мы пишем отдельно, она называется frontend - фронтенд.
REST работает с помощью протокола HTTP
, по сути этот протокол позволяет передавать
любые данные, но основу представляют текстовые файлы. Грубо говоря, мы обращаемся
к серверу с тем или иным запросом, а он в ответ возвращает нам текстовый файл.
Каждая точка доступа для запроса называется endpoint - конечная точка, А то, к чему она даёт доступ - ресурс.
Любая конечная точка в своей сути - просто веб-адрес. Веб-адрес состоит из ip или домена сервера и пути до неё:
http://web.site/rest/request1?arg1=val1&arg2=val2
http://web.site/
- это адрес сайта,rest/request1
- URI,arg1=val1&arg2=val2
- параметры запроса
Путь к ресурсу rest/request1
называется URI,
расшифровывается как Uniform Resource Identifier - универсальный
идентификатор ресурса.
Параметры запроса передаются после знака вопроса в виде аргумент=значение
,
значения разделяются символом амперсанта &
.
URI в REST принято начинать с множественной формы существительного, описывающего ресурс.
Допустим, что в приложении списка дел есть ещё и авторы задачи. Тогда, чтобы получить список всех пользователей, следует использовать URI
/users
Если мы хотим получить конкретного пользователя с id 1
/users/1
Чтобы запросить список дел конкретного пользователя
/users/1/tasks
Получить конкретную задачу конкретного пользователя
/users/1/tasks/4
Цифры - это id выбранной сущности
Необязательно называть URI так, как я назвал, просто так принято, чтобы люди, использующие ваш код, быстрее в нём могли разобраться. В сущность сама архитектура REST для этого и разработана.
Обработка запросов
Для начала напишем простейший обработчик запросов.
Для этого нам нужно создать специальный класс-контроллер. Для этого
создадим пакет controller
и в нём класс TasksController
.
Можно было бы написать всю логику обработки в нём, но такой подход слишком негибок. Гораздо лучше всю логику работы именно с запросами оставить в нём, а сами действия, которые должны быть выполнены по тому или иному запросу в отдельном классе-сервисе.
Сервисы удобны тем, что мы можем задекларировать все методы, которые тот или иной сервис должен реализовывать, а потом просто реализовать эти методы для каждого из нужных нам сервисов.
Пока что у нас будет всего один сервис DefaulTaskService
. Он будет расширять
интерфейс TaskService
- который описывает интерфейс всех сервисов,
работающих с задачами. Создадим их в пакете service
Дерево проекта теперь будет выглядеть так:
- TaskService.java
package com.example.demo.service;
import com.example.demo.entities.Tasks;
import java.util.List;
/**
* Сервис задач
*/
public interface TasksService {
/**
* Создает новой задачи
* @param task - задача для создания
*/
void create(Tasks task);
/**
* Возвращает список всех имеющихся задач
* @return список задач
*/
List<Tasks> readAll();
/**
* Возвращает задачу по её ID
* @param id - ID задачи
* @return - объект задачи с заданным ID
*/
Tasks read(int id);
/**
* Обновляет задачу с заданным ID,
* в соответствии с переданной задачей
* @param task - задача в соответсвии с которой нужно обновить данные
* @param id - id задачи которого нужно обновить
* @return - true если данные были обновлены, иначе false
*/
boolean update(Tasks task, int id);
/**
* Удаляет задачу с заданным ID
* @param id - id задачи, которую нужно удалить
* @return - true если клиент был удален, иначе false
*/
boolean delete(int id);
}
Пропишем сервис обработки задач по умолчанию. Чтобы сказать spring,
что это сервис, добавим в объявление класс аннотацию @Service
Для начал не будем хранить задачи в базе данных. Вместо этого
мы создадим словарь Map<Integer, Tasks>
, в котором ключ - это
id
объекта, а значение - задача с этим id
.
Так делается для эффективного хранения данных. Id может быть сразу равен
довольно большому значению. Если мы будем хранить данные в списке на массиве,
например, и сохранять объекты с индексом, равным id
. Но тогда
нам пришлось бы выделить память под все несуществующие элементы, имеющие
индекс, меньший заданного.
Атомарные типы данных
Наш сервис запускает асинхронно. REST-запрос может прийти в любой момент, запросы даже могут прийти практически одновременно. Получается, что нам нужно выполнять ту или иную подпрограмму по событию.
Можно было бы сделать бесконечный цикл и проверять раз в заданное время, не получены ли новые данные. Но такой подход неэффективен. Во-первых, мы можем пропустить тот самый момент, когда запрос пришёл, во-вторых, сама по себе практика бесконечных циклов с задержкой порочна.
Довольно давно была разработана концепция многопоточных приложений. Такие приложения состоят из нескольких программ-потоков, которые могут запуститься, когда нам нужно и остановиться. Получается, что в одном приложении у нас как бы одновременно работает несколько программ.
В действительности это не так. Операционная система просто переключается между программами и передаёт его ресурсы то одному, то другому потоку.
Если два потока работают с одной и той же переменной, то может случиться коллизия, например: первый ещё не дописал байты, а второй уже читает. При работе с примитивными данными кроме как к неправильным данным это не приведёт, а вот со сложными структурами это может привести кошибке исполнения.
Поэтому при совместном использовании ресурсов разными потоками эти ресурсы синхронизируют. Синхронизация подразумевает, что мы приостанавливаем выполнение потока, если он обращается к синхронизированному ресурсу, который уже используется другим потоком.
Как только ресурс освободится, ожидающий его освобождения поток запускается и продолжает свою работу. К сожалению такой подход череват взаимной блокировкой(deadlock).
Взаимная блокировка происходит, когда один поток занял ресурс и ждёт, пока осводится второй ресурс, но он занят другим потоком, который в свою очередь ждёт освобождения первого ресурса.
Чтобы избежать этого, старайтесь вместо синхронизации использовать атомарные типы данных. Атомарные типы данных - это классы, каждый метод в которых занимает один такт процессора. Если операция выполняется за один такт, тогда синхронизация не нужна. Ведь на следующем такте процессора, путь он и ывполняет другой поток, ресурс уже будет свободен.
В нашей задаче нужно асинхронно генерировать целочисленный id
. Для этого лучше
всего подойдёт класс AtomicInteger
. Это некий аналог класса-обёртки Integer
.
Нам будет нужен метод incrementAndGet()
. Он сначала увеличивает внутреннее значение на 1, а потом возвращает его.
Каждый раз вызывая этот метод из любого потока мы будем получать новое число, большее
предыдущего на 1.
- DefaultTaskService.java
package com.example.demo.service;
import com.example.demo.entities.Tasks;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Сервис задачи по умолчанию
*/
@AllArgsConstructor
@Service
public class DefaultTasksService implements TasksService {
/**
* Хранилище задач
*/
private static final Map<Integer, Tasks> TASK_REPOSITORY_MAP = new HashMap<>();
/**
* Генератор id
*/
private static final AtomicInteger TASK_ID_GENERATOR = new AtomicInteger();
/**
* Создает новую задачу
*
* @param task - задача для создания
*/
@Override
public void create(Tasks task) {
final int TaskId = TASK_ID_GENERATOR.incrementAndGet();
task.setId(TaskId);
TASK_REPOSITORY_MAP.put(TaskId, task);
}
/**
* Возвращает список всех имеющихся задач
*
* @return список задач
*/
@Override
public List<Tasks> readAll() {
return new ArrayList<>(TASK_REPOSITORY_MAP.values());
}
/**
* Возвращает задачу по её ID
*
* @param id - ID задачи
* @return - объект задачи с заданным ID
*/
@Override
public Tasks read(int id) {
return TASK_REPOSITORY_MAP.get(id);
}
/**
* Обновляет задачу с заданным ID,
* в соответствии с переданной задачей
*
* @param task - задача в соответсвии с которой нужно обновить данные
* @param id - id задачи которого нужно обновить
* @return - true если данные были обновлены, иначе false
*/
@Override
public boolean update(Tasks task, int id) {
// если в словаре есть указанный `id`
if (TASK_REPOSITORY_MAP.containsKey(id)) {
// задаём задаче этот id
task.setId(id);
// помещаем новую задачу в словарь
TASK_REPOSITORY_MAP.put(id, task);
return true;
}
return false;
}
/**
* Удаляет задачу с заданным ID
*
* @param id - id задачи, которую нужно удалить
* @return - true если клиент был удален, иначе false
*/
@Override
public boolean delete(int id) {
return TASK_REPOSITORY_MAP.remove(id) != null;
}
}
Бэкенд
Теперь пропишем сам контроллер. Он должен использовать функционал, который реализован в сервисе. У нас есть два пути: первый - это наследование, мы могли бы унаследовать контроллер от сервиса, но в spring принято использовать внедрение зависимостей. В этом случае мы просто создаёт поле сервиса, а потом передаём его в качестве аргумента.
Чтобы вручную не прописывать инструкции для spring, как именно следует понимать наше
поле, в конструкторе нужно просто приписать аннотацию @Autowired
Чтобы наш контроллер обрабатывал REST-запросы, ему необходимо приписать аннотацию
@RestController
. Немного выше была описана логика составления URI. Согласно ей
все запросы к задачам должны начинаться с tasks/
, это мы укажем с помощью
аннотации @RequestMapping("/tasks")
.
Также нам понадобится третья аннотация @Log
. Она позволяет вызывать команды
добавления в лог без дополнительного кода log.info("текст сообщения")
. Переменная
логирования создаётся за нас.
У spring есть готовые инструменты логирования. По умолчанию он выводит лог в консоль
Но можно и перенаправить его вывод, например, в файл.
У лога есть разные уровни: .info()
- информация, .warning()
- предупреждение. Есть
и другие уровни, но здесь мы их рассматривать не будем.
- TaskController.java
package com.example.demo.controller;
import com.example.demo.entities.Tasks;
import com.example.demo.service.TasksService;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Контроллер задач
*/
@RestController
@RequestMapping("/tasks")
@Log
public class TasksController {
/**
* Сервис задач
*/
private final TasksService tasksService;
/**
* Конструктор контроллера задач
*
* @param tasksService сервис задач
*/
@Autowired
public TasksController(TasksService tasksService) {
this.tasksService = tasksService;
}
/**
* Создать задачу
*
* @param task - задача
* @return - ответ на REST запрос
*/
@PostMapping(value = "/create")
public ResponseEntity<?> create(@RequestBody Tasks task) {
tasksService.create(task);
log.info("Задача " + task.getTitle() + " создана");
return new ResponseEntity<>(HttpStatus.CREATED);
}
/**
* Получить список всех задач
*
* @return - ответ на REST запрос
*/
@GetMapping(value = "/list")
public ResponseEntity<List<Tasks>> readAll() {
// получаем все задачи
final List<Tasks> tasks = tasksService.readAll();
// выводим в лог, сколько задач найдено
log.info("Найдено " + tasks.size() + " задач");
// если есть хотя бы одная задача
return !tasks.isEmpty()
? new ResponseEntity<>(tasks, HttpStatus.OK) // возвращаем в ответе список задач, статус ответа `OK`
: new ResponseEntity<>(HttpStatus.NOT_FOUND); // иначе статус ответа `NOT_FOUND`
}
/**
* Получить задачу по id
*
* @param id - id задачи
* @return - ответ на REST запрос
*/
@GetMapping(value = "/list/{id}")
public ResponseEntity<Tasks> read(@PathVariable(name = "id") int id) {
// ищем задачу по id
final Tasks task = tasksService.read(id);
// выводим сообщение о том, какая задача запрошена
log.info("Запрошена задача " + task);
// если задача найдена
return task != null
? new ResponseEntity<>(task, HttpStatus.OK) // возвращаем в ответе задачу, статус ответа `OK`
: new ResponseEntity<>(HttpStatus.NOT_FOUND); // иначе статус ответа `NOT_FOUND`
}
/**
* Обновить заданную задачу
*
* @param task новая задача
* @param id - id старой задачи
* @return - ответ на REST запрос
*/
@PostMapping(value = "/update/{id}")
public ResponseEntity<Tasks> update(@RequestBody Tasks task, @PathVariable(name = "id") int id) {
// выводим инофрмацию, какая задача будет модифицирована
log.info("Обновляется задача с id=" + id + " новые данные: " + task);
// если получилось обновить задачу
return tasksService.update(task, id)
? new ResponseEntity<>(task, HttpStatus.OK) // возвращаем в ответе задачу, статус ответа `OK`
: new ResponseEntity<>(HttpStatus.NOT_FOUND); // иначе статус ответа `NOT_FOUND`
}
/**
* Удалить задачу
*
* @param id - id задачи
* @return - ответ на REST запрос
*/
@PostMapping(value = "/delete/{id}")
public ResponseEntity<Tasks> delete(@PathVariable(name = "id") int id) {
// выводим сообщение с информацией о том, какую задачу нужно удалить
log.info("Задача с id=" + id + " удалена");
// если получилось удалить задачу
return tasksService.delete(id)
? new ResponseEntity<>(HttpStatus.OK) // статус ответа `OK`
: new ResponseEntity<>(HttpStatus.NOT_FOUND); // иначе статус ответа `NOT_FOUND`
}
}
Каждый метод в нашем контроллере - это обработчик того или иного запроса. Для того чтобы указать, какой
именно запрос обрабатывает тот или иной метод, используется аннотации @PostMapping
и
@GetMapping
. Первая показывает, что метод обрабатывает POST
-запросы, вторая - что GET
.
На каждый POST
или GET
запрос обязательно должен быть сформирован ответ.
Поэтому у каждого
метода возвращаемое значение - это ResponseEntity<>
, построенный на основе того или иного объекта.
В своей сути ResponseEntity<>
- это просто обёртка, которая помимо объекта возвращает статус
запроса.
Чтобы вычленить из запроса число, соответсвующее id
, в URI используется шаблон {id}
,
а аргумент метода аннотируется как @PathVariable()
. Если метод принимает какие-то объекты,
переданные в теле запроса, то необходимо эти аргументы аннотировать как @RequestBody
.
Postman
Чтобы проверить работу нашего сервера, нам понадобится инструмент для формирования запросов. Таких инструментов довольно много, но самым популярным является программа Postman.
Установка
Скачать установщик можно с официального сайта, либо с моего зеркала.
Postmnan установится автоматически
Теперь вам необходимо создать учётную запись. Для этого нажмите Create Free Account
. Если
у вас уже есть учётная запись, просто нажмите Sign In
. Можно не создавать отдельную учётную запись,
а войти с помощью учётной записи Google. Для этого нажмите Sign in
Программа будет ожидать, пока вы войдёте в учётную запись в браузере
Чтобы войти через Google, кликните по кнопке Sign in with Google
Если у вас нет такой учётной записи, можно зарегистрироваться в системе Postman. Для этого в главном
окне программы нажмите кнопку Create free account
. В новом окне введите свои:
- почту
- имя пользователя
- пароль
Нажмите на кнопку Create free account
После регистрации браузер предложит вам вернуться к приложению. Нажмите Открыть приложение Postman
Если браузер сам не вернёт вас к программе, вернитесь в неё вручную
Дождитесь загрузки программы и в новом окне введите своё имя и роль. Я поставил Fullstack Developer
.
Вы можете выбрать любую другую. Это не имеет значения для дальнейшей работы.
В следующем окне постман предложит "пригласить друзей". Просто пропустите этот шаг и нажмите на кнопку
Continue without team
Готово. Программа запущена и может отправлять запросы. Чтобы убедиться в
этом, нажмите на кнопку Send
.
Ответ сформируется в нижней части окна
Добавление задачи
Теперь проверим наш бэкенд.
Сначала создадим несколько задач. Я сделал метод создания POST
, потому что он позволяет
защититься от взлома. Если же нужно просто прочитать, т.е. использовать GET
, то даже если кто-то "вклинится" в обмен
сообщениями, ничего кроме чтения той же информации, что и пользователь, он не получит.
А вот доступ к POST
методу позволит менять данные на сайте. Конечно, помимо чтения и записи,
POST
и GET
запросы используются и в других задачах. Но разделение именно такое. Безопасные запросы
реализовывают через GET
, а через POST
- требующие защиты от злоумышленников.
Запустим сервер, и сформируем запрос. Чтобы передать объект, его необходимо представить в json
формате.
По умолчанию выбран режим GET
запросов. Для добавления задачи нам нужен метод POST
. Кликните
по галочке справа от GET
В выпадающем списке выберите POST
Теперь необходимо указать URI запроса. В нашем случае - это
http://localhost:8080/tasks/create
Чтобы добавить объект в тело запроса, откройте вкладку Body
json-представление первой задачи укажем таким:
{
"title":"task1",
"author":"user",
"text":"test task"
}
Нажмите кнопку Send
Если всё в порядке, то в логе сервера вы увидите строчку с информацией о добавлении
Если у вас не запущен сервер, то постман выведет такое сообщение об ошибке
Список задач
Чтобы получить список задач, отправим GET
запрос
http://localhost:8080/tasks/list
Получим список из одной задачи.
Теперь добавим ещё одну. Для этого воспользуемся уже отправленным запросом, чтобы не писать всё с нуля. История запросов находится в левом углу программы. Кликните по первому отправленному запросу
Тело запроса сформируется за нас. Просто поменяем значение заголовка и текста
{
"title":"task2",
"author":"user",
"text":"test task 2"
}
Таким же способом добавим третью задачу:
{
"title":"task3",
"author":"user",
"text":"test task 3"
}
Теперь вызовем снова метод GET
из истории запросов:
Теперь прочитаем задачу с id
равным 1
. Запрос будет таким
http://localhost:8080/tasks/list/1
В ответ получим:
{
"id": 1,
"title": "task1",
"author": "user",
"text": "test task"
}
Теперь поменяем заголовок у задачи с id
равным 2
. Т.к. мы изменяем данные бэкенда,
запрос должен быть POST
http://localhost:8080/tasks/update/2
Тело запроса будет таким:
{
"title":"task2",
"author":"user",
"text":"modified test task"
}
В ответ получим:
{
"id": 2,
"title": "task2",
"author": "user",
"text": "modified test task"
}
Удалим теперь задачу с id
равным трём. Отправляем POST
запрос с пустым телом
http://localhost:8080/tasks/delete/3
И снова выведем список задач:
http://localhost:8080/tasks/list
Получим ответ:
[
{
"id": 1,
"title": "task",
"author": "user",
"text": "test task"
},
{
"id": 2,
"title": "task2",
"author": "user",
"text": "modified test task"
}
]
Подключение БД
Пока что наш сервер хранит все данные в оперативной памяти, поэтому при перезагрузке они будут утеряны. Чтобы данные не зависели от программы, их переносят в базу данных(БД).
Мы уже выполнили всю настройку, теперь осталось только переписать сервис работы с задачами.
Чтобы работать с базами данных в спринг используется подход репозиториев. Чтобы создать репозиторий
для работы с PostgreSQL, достаточно просто наследовать интерфейс JpaRepository<>
. Вся логика для
работы с данными уже прописана за нас.
Создадим пакет repository
и добавим в него интерфейс TasksRepository
- TasksRepository.java
package com.example.demo.repository;
import com.example.demo.entities.Tasks;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TasksRepository extends JpaRepository<Tasks, Integer> {
}
Теперь перепишем сам сервис:
- DefaultTaskService.java
package com.example.demo.service;
import com.example.demo.entities.Tasks;
import com.example.demo.repository.TasksRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Сервис задач по умолчанию
*/
@Service
public class DefaultTasksService implements TasksService {
/**
* Репозиторий базы данных задач
*/
private final TasksRepository tasksRepository;
/**
* Создает новую задачу
*
* @param task - задача для создания
*/
@Override
public void create(Tasks task) {
tasksRepository.save(task);
}
/**
* Конструктор сервиса задач
*
* @param tasksRepository репозиторий збд задач
*/
@Autowired
public DefaultTasksService(TasksRepository tasksRepository) {
this.tasksRepository = tasksRepository;
}
/**
* Возвращает список всех имеющихся задач
*
* @return список задач
*/
@Override
public List<Tasks> readAll() {
return tasksRepository.findAll();
}
/**
* Возвращает задачу по её ID
*
* @param id - ID задачи
* @return - объект задачи с заданным ID
*/
@Override
public Tasks read(int id) {
return tasksRepository.getById(id);
}
/**
* Обновляет задачу с заданным ID,
* в соответствии с переданной задачей
*
* @param task - задача в соответсвии с которой нужно обновить данные
* @param id - id задачи которого нужно обновить
* @return - true если данные были обновлены, иначе false
*/
@Override
public boolean update(Tasks task, int id) {
// если существует задача с таким id
if (tasksRepository.existsById(id)) {
// меняем id у задачи
task.setId(id);
// при сохранении в базу данных будет найдена запись с таким же
// id, как у сохраняемого, после чего все поля записи будут приведены в
// соответствие с полями переданного объекта
tasksRepository.save(task);
return true;
}
return false;
}
/**
* Удаляет задачу с заданным ID
*
* @param id - id задачи, которую нужно удалить
* @return - true если клиент был удален, иначе false
*/
@Override
public boolean delete(int id) {
// если существует задача с таким id
if (tasksRepository.existsById(id)) {
// удаляем её
tasksRepository.deleteById(id);
return true;
}
return false;
}
}
Снова запустим сервер и отправим ему 4 запроса на создание задач:
http://localhost:8080/tasks/create
- Task1
- Task2
- Task3
- Task4
{
"title":"task1",
"author":"user",
"text":"test task"
}
{
"title":"task2",
"author":"user",
"text":"test task2"
}
{
"title":"task3",
"author":"user",
"text":"test task3"
}
{
"title":"task4",
"author":"user",
"text":"test task4"
}
Теперь снова загрузим список:
http://localhost:8080/tasks/list
Получим:
[
{
"id": 0,
"title": "task1",
"author": "user",
"text": "test task"
},
{
"id": 2,
"title": "task3",
"author": "user",
"text": "test task3"
},
{
"id": 3,
"title": "task4",
"author": "user",
"text": "test task4"
},
{
"id": 1,
"title": "task2",
"author": "user",
"text": "test task2"
}
]
Теперь откроем редактор таблиц БД в idea. Для этого в правом углу нажмите на вкладку Database
и дважды кликните по
таблице tasks-table
Сейчас таблица задач выводится так, как хранится. Если вам нужно упорядочить таблицу по тому или
иному полю, просто кликните по нему. Упорядочим таблицу по id
, для этого кликните по соответствующему
заголовку.
Спустя некоторое время таблица обновится и будет уже упорядоченной
Если кликнуть ещё раз, то таблица упорядочится в обратном порядке
Чтобы вручную изменить то или иное значение в таблице, кликните по нему два раза. Появится окно редактирования. Добавим таким образом пробелы в текст задач.
Все изменённые значения таблицы специально подсвечиваются. После того как мы выгрузим изменения, они снова вернутся к обычному виду. Чтобы применить изменения, нажмите на зелёную стрелочку (не путайте с кнопкой запуска приложения).
После обработки запроса, таблица будет выглядеть так
Проверим, что изменения применились. Для этого снова сделаем запрос
http://localhost:8080/tasks/list
Получим
[
{
"id": 0,
"title": "task1",
"author": "user",
"text": "test task"
},
{
"id": 3,
"title": "task4",
"author": "user",
"text": "test task 4"
},
{
"id": 2,
"title": "task3",
"author": "user",
"text": "test task 3"
},
{
"id": 1,
"title": "task2",
"author": "user",
"text": "test task 2"
}
]
Значения изменились, всё работает как надо. Оставшиеся URI проверьте самостоятельно.