# Глава 1.6. Состояние приложения Хендлерам часто нужны общие данные: подключение к базе данных, конфигурация приложения, клиент для внешнего API, кеш. Такие данные называют состоянием приложения. Создавать их заново на каждый запрос дорого, а глобальные переменные неудобны: их сложно подменить в тестах, и по сигнатуре хендлера не видно, от чего он зависит. В Axum состояние передаётся в хендлеры явно, через экстрактор `State`. ## Передача состояния в хендлер Состояние описывается обычной структурой. Её передают роутеру методом `with_state`, а хендлер получает её через экстрактор `State<T>`: ```rust use axum::{extract::State, routing::get, Router}; #[derive(Clone)] struct AppState { greeting: String, } async fn hello(State(state): State<AppState>) -> String { state.greeting } #[tokio::main] async fn main() { let state = AppState { greeting: "Привет!".to_string(), }; let router = Router::new() .route("/", get(hello)) .with_state(state); // запуск сервера // ... } ``` Запрос `GET /` вернёт строку `Привет!`. Тип состояния указан прямо в сигнатуре хендлера, поэтому сразу видно, какие данные ему нужны. ## Проверка состояния при компиляции Axum проверяет, что состояние передано роутеру, ещё на этапе компиляции. Пока `with_state` не вызван, роутер имеет тип `Router<AppState>`, то есть «роутер, которому ещё нужно состояние типа `AppState`». Метод `with_state` превращает его в `Router<()>`, и только такой роутер можно передать в `axum::serve`. Если забыть вызвать `with_state`, программа не скомпилируется: ```text error[E0277]: the trait bound `for<'a> Router<AppState>: Service<IncomingStream<'a, TcpListener>>` is not satisfied ``` Сообщение выглядит пугающе, но главное в нём — тип `Router<AppState>`: он означает, что роутер всё ещё ждёт состояние. ## Клонирование состояния Тип состояния обязан реализовывать трейт `Clone`: Axum клонирует состояние для каждого запроса, чтобы у каждого хендлера была своя копия. Без `Clone` компилятор выдаст ошибку: ```text error[E0277]: the trait bound `AppState: Clone` is not satisfied ``` Клонировать строку на каждый запрос недорого, но у клонирования есть две проблемы. Во-первых, большие данные, например загруженный в память справочник, копировать на каждый запрос дорого. Во-вторых, у каждого запроса оказывается своя копия, и изменения, сделанные в одном хендлере, другие не увидят. Эти проблемы решает [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html). При клонировании `Arc` копируется только указатель и увеличивается счётчик ссылок, а сами данные остаются в одном экземпляре: ```rust use std::sync::Arc; #[derive(Clone)] struct AppState { catalog: Arc<Vec<String>>, } ``` ## Изменяемое состояние `Arc` даёт общий доступ к данным только на чтение. Если хендлеры должны изменять общие данные, их нужно защитить от одновременного изменения из нескольких потоков. Для этого используют [`Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html): он пропускает к данным только один поток за раз, а остальные ждут своей очереди. Например, так выглядит список пользователей в памяти: ```rust use std::sync::{Arc, Mutex}; use axum::{extract::State, http::StatusCode, Json}; #[derive(Clone)] struct AppState { users: Arc<Mutex<Vec<String>>>, } async fn list_users( State(state): State<AppState>, ) -> Result<Json<Vec<String>>, StatusCode> { let users = state .users .lock() .unwrap(); Ok(Json(users.clone())) } async fn create_user( State(state): State<AppState>, Json(user): Json<CreateUser>, ) -> Result<StatusCode, StatusCode> { let mut users = state .users .lock() .unwrap(); users.push(user.name); Ok(StatusCode::CREATED) } ``` Метод `lock` захватывает мьютекс и возвращает защитный объект, через который можно работать с данными. Мьютекс освобождается автоматически, когда этот объект выходит из области видимости. Проверим: ```bash $ curl -X POST -H 'content-type: application/json' \ -d '{"name":"Alice"}' http://127.0.0.1:3000/users $ curl http://127.0.0.1:3000/users ["Alice"] ``` Такое хранилище живёт только в памяти процесса и пропадает при перезапуске сервера. Для учебных примеров этого достаточно, а настоящие данные мы будем хранить в базе данных. ## Часть состояния По мере роста приложения у состояния появляется всё больше полей, а конкретному хендлеру обычно нужна только их часть. Чтобы хендлер зависел только от того, что ему действительно нужно, Axum позволяет извлекать из состояния отдельные поля. Для этого нужен макрос `FromRef`, который подключается фичей `macros`: ```bash cargo add axum --features macros ``` ```rust use axum::extract::{FromRef, State}; #[derive(Clone)] struct Config { greeting: String, } #[derive(Clone, FromRef)] struct AppState { config: Config, users: Arc<Mutex<Vec<String>>>, } async fn hello(State(config): State<Config>) -> String { config.greeting } ``` Роутеру по-прежнему передаётся всё состояние `AppState`: ```rust let state = AppState { config: Config { greeting: "Привет!".to_string(), }, users: Arc::new(Mutex::new(Vec::new())), }; let router = Router::new() .route("/", get(hello)) .with_state(state); ``` А хендлер `hello` при этом получает только `Config`. `FromRef` генерирует для каждого поля способ достать его из `AppState`, поэтому типы полей должны реализовывать `Clone`. В следующей главе разберём, как описать собственный тип ошибки и превращать его в понятные клиенту HTTP-ответы.
Наша группа в telegram. Задавайте вопросы и общайтесь.
Задонатить. Поддержите развитие площадки.