Java: Веб-технологии

Теория: Динамические маршруты

До сих пор мы встречались только со статическими маршрутами. В таких маршрутах нет изменяемых частей — адрес точно совпадает с маршрутом и не меняется. На практике чаще встречаются динамические маршруты. Для примера проанализируем адреса курсов на Хекслете:

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

Динамические маршруты

Вернемся еще раз к адресам курсов на Хекслете:

Обратите внимание, что в этих адресах прослеживается определенная структура:

/courses/<имя курса>

Для таких адресов создается ровно один маршрут, в котором изменяемая часть попадает внутрь через контекст как параметр:

package org.example.hexlet;

import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
        });

        // Обратите внимание, что id — это не обязательно число
        app.get("/courses/{id}", ctx -> {
            ctx.result("Course ID: " + ctx.pathParam("id"));
        });
        app.get("/users/{id}", ctx -> {
            ctx.result("User ID: " + ctx.pathParam("id"));
        });

        app.start(7070);
    }
}

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

/courses/{id}

В этом маршруте секция {id} означает, что на это место подставляется конкретный идентификатор курса:

curl localhost:7070/courses/132
Course id: 132

curl localhost:7070/courses/php-oop
Course id: php-oop

Имя изменяемой части можно выбирать произвольно — например, вместо {id} можно написать {lala}. Способ записи зависит от конкретного фреймворка. Здесь мы записали имя с обрамляющими фигурными скобками {}, как это принято в Javalin.

Параметры пути

Изменяемая часть маршрута в Javalin называется параметром пути. В примере выше есть только один такой параметр — это id. Доступ к нему мы получаем через метод ctx.pathParam():

// String
var id = ctx.pathParam("id");

Как и в случае с параметрами запроса, возвращаемый тип метода ctx.pathParam() будет равен String. Это подходит не для всех случаев. Поэтому ctx содержит метод, который позволяет автоматически конвертировать данные в нужный тип:

var id = ctx.pathParamAsClass("id", Integer.class);

Откуда берутся конкретные значения в параметрах пути? Конкретные ссылки формируются на страницах сайта. Для примера можно посмотреть на список постов в блоге Хекслета. На каждый пост есть ссылка, которая формируется в коде как обычная строчка с подстановкой:

var url = "/blog/posts/" + post.getId()

Чтобы пользователям было удобнее, разработчики стараются использовать в адресах не числовые идентификаторы, а человекочитаемые названия. Например, вместо /courses/332 показывают /courses/php-mvc. Эту часть адреса называют словом слаг (slug). В каждом случае мы используем уникальный слаг, при этом его формат обязан соответствовать требованиям формирования адресов.

Как правило, такие имена содержат символы латинского алфавита с дефисами между словами:

this-that-other-outre-collection

Подведем промежуточный итог:

  • Понятия «адрес» и «маршрут» обозначают разные вещи
  • Если маршрут статический, то он всегда совпадает с адресом — например, /about
  • Если маршрут динамический, то ему могут соответствовать бесконечное число адресов, даже если таких страниц на сайте нет — например, /courses/

Обработка ошибок

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

Предположим, что в нашей базе данных есть 10 курсов c идентификаторами от 1 до 10. Адрес каждого курса формируется так:

/courses/{id}

В таком случае адрес /courses/1 вернет курс с идентификатором 1. А вот адрес /courses/11 выдаст ошибку 404, потому что такого курса не существует. Чтобы программа сработала именно так, мы должны все правильно реализовать. По идее, код должен выполнять два обязательных действия:

  • Проверять наличие данных в базе
  • Выбрасывать исключения в тех случаях, когда данных нет

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

// Дополнительный импорт
import io.javalin.http.NotFoundResponse;

// Обработчик маршрута /courses/{id}
public static void show(Context ctx) {
    var id = ctx.pathParamAsClass("id", Long.class).get();
    // Позже мы разберем эти конструкции подробнее
    var user = UserRepository.find(id) // Ищем пользователя в базе по id
            .orElseThrow(() -> new NotFoundResponse("Entity with id = " + id + " not found"));
}

Множественные параметры пути

Иногда в маршруте может быть более одного параметра. Обычно такие маршруты используются для вложенных ресурсов. Именно так работает пример ниже, где уроки вложены в курсы:

package org.example.hexlet;

import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
        });

        // Название параметров мы выбрали произвольно
        app.get("/courses/{courseId}/lessons/{id}", ctx -> {
            var courseId = ctx.pathParam("courseId");
            var lessonId =  ctx.pathParam("id");
            ctx.result("Course ID: " + courseId + " Lesson ID: " + lessonId);
        });

        app.start(7070);
    }
}

Порядок определения

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

Посмотрите на этот пример:

app.get("/courses/{id}", ctx -> {
    // Вывод информации о курсе
});
app.get("/courses/build", ctx -> {
    // Вывод формы создания курса
});

При таком порядке программа обработает запрос на страницу /courses/build через маршрут /courses/{id}. Это не то, что мы задумывали. Чтобы исправить, поменяем маршруты местами:

app.get("/courses/build", ctx -> {
    // Вывод формы создания курса
});

app.get("/courses/{id}", ctx -> {
    // Вывод информации о курсе
});

Ситуация с неверным обработчиком повторится, если мы добавим курс, в которой значение id равно build. Чтобы предотвратить эту проблему, мы просто запрещаем создавать такие id.

Рекомендуемые программы