深入理解 Rust Axum:兩種依賴注入模式的實踐與對比(二)

前言

我想把使用 Rust 開發Websocket 服務的文章寫成一個系列,前面寫了一遍如何使用 Axum 搭建一個Websocket 服務的文章,我們可以和前端demo頁面進行全雙工的 Websocket 消息傳輸,而且可以啟用 HTTP2 的同時啟用 TLS。

這時候問題來了,Axum Web 應用和 Java Spring Web 應用一樣,在 Axum 中如何依賴其他對象或資源呢。Websocket 服務也是Web服務,面對不同的連客戶端接請求,每個連接請求有著不同的后端邏輯。這些后端的邏輯Service如果在每個Websocket 連接處理器中去分別創建Service對象或者數據庫對象就會大大拉低服務性能,也會占用更多的內存。能不能像 Java Web 生態中 Spring 框架的單例Bean那樣去做依賴注入呢?

我的開發項目:RTMate

GitHub地址:https://github.com/BruceZhang54110/RTMate

首先什么是依賴注入?

首先什么是依賴注入?

在軟件工程中,依賴注入(dependency injection)的意思為,給予調用方它所需要的事物。“依賴”是指可被方法調用的事物。依賴注入形式下,調用方不再直接指使用“依賴”,取而代之是“注入” 。“注入”是指將“依賴”傳遞給調用方的過程。在“注入”之后,調用方才會調用該“依賴”。傳遞依賴給調用方,而不是讓讓調用方直接獲得依賴,這個是該設計的根本需求。在編程語言角度下,“調用方”為對象和類,“依賴”為變量。在提供服務的角度下,“調用方”為客戶端,“依賴”為服務。

以Java 語言為例,當 class A 使用 class B 的某些功能時,則表示 class A 具有 class B 依賴。在使用其他 class 的方法之前,我們首先需要創建那個 class 的對象(即 class A 需要創建一個 class B 實例)。

因此,將創建對象的任務轉移給其他 class,并直接使用依賴項的過程,被稱為“依賴注入”。

為什么需要依賴注入?

如果要在 Axum Websocket 服務中要保證創建的對象是單例的,并且可以有一個全局上下文,有一個bean的資源池,有連接請求需要處理時可以直接拿到全局的單例對象去操作,需要如何實現呢?這就是今天我想在這里討論的內容。

依賴注入的好處
  1. 避免在每個請求中重復創建昂貴的對象(如數據庫連接池、外部服務客戶端),從而降低服務性能開銷和內存占用。
  2. 確保某些服務或資源在整個應用生命周期中只有一個實例,統一管理狀態和行為。
  3. 方便模擬(mock)或替換依賴,進行單元測試。
  4. 依賴關系清晰,修改和重構更容易。
  5. 在不修改代碼的情況下替換不同實現。
  6. 集中管理依賴的生命周期和實例化。

Axum 簡介

Rust 生態中一個流行的 Web 框架,以其簡潔、高性能和對異步處理的良好支持而聞名。充分利用 tower 和 tower-http 的中間件、服務和工具生態系統。

在Rust Axum框架中,使用 Router(路由) 來創建接口, 和 Java類比的話,那就是Java Spring Web項目中Controller類中定義的接口。創建 Router 時就要指定這個接口對應的 handler 方法。

  • 使用 Route 定義接口
  • 使用 handler 定義調用接口要執行的方法
  • 使用 Extractors 解析傳入的請求參數
  • 在 handlers 之間共享 state

如何在 Axum 不同 handlers 之間共享單例對象

如何讓每個請求訪問共同一份數據

要實現Web應用的依賴注入,首先要保證注入的資源是單例的,是共享的。如何實現,答案就是使用Axum的在handlers 之間共享狀態的辦法。在 Axum 文檔中寫了四種 在handlers 之間共享狀態的方法:

  1. Using the State extractor:使用 State 提取器
  2. Using request extensions:使用請求擴展
  3. Using closure captures:使用閉包去捕獲
  4. Using task-local variables:使用任務局部變量

我們今天使用最常見的使用 State 提取器(axum::extract::State)。作用是將應用級別的共享狀態(通常是一個結構體,其中包含各種單例服務)通過 Routerwith_state 方法綁定,然后在處理器中通過 State<T> 提取。這個state 就是一個全局共享的狀態,用來管理整個應用的全局狀態和單例服務。

官網的簡寫代碼示例如下,struct AppState 定義我們想要全局依賴的內容,使用 Arc 創建原子引用計數的 shared_state ,再傳到 with_state 中。這樣在每個handler 中,都能拿到一個 state 在這里就是 State<Arc<AppState>>,這就達到了多個handler 共享 AppState 的目的。

use axum::{ extract::State,routing::get,Router,
};
use std::sync::Arc;struct AppState {// ...
}let shared_state = Arc::new(AppState { /* ... */ });let app = Router::new().route("/", get(handler)).with_state(shared_state);async fn handler(State(state): State<Arc<AppState>>,
) {// ...
}

使用Rust為我們帶來兩種依賴注入方式

代碼來自于 Axum Github 代碼倉庫的依賴注入示例。我們定義一個 User Repo 用來查詢用戶和創建用戶。提供可擴展的 Trait UserRepo。通過依賴注入模式,我們可以根據需要注入不同的 UserRepo。

use std::{collections::HashMap,sync::{Arc, Mutex},
};use axum::{extract::{Path, State},http::StatusCode,routing::{get, post},Json, Router,
};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid;#[tokio::main]
async fn main() {tracing_subscriber::registry().with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),).with(tracing_subscriber::fmt::layer()).init();let user_repo = InMemoryUserRepo::default();let using_dyn = Router::new().route("/users/{id}", get(get_user_dyn)).route("/users", post(create_user_dyn)).with_state(AppStateDyn {user_repo: Arc::new(user_repo.clone()),});let using_generic = Router::new().route("/users/{id}", get(get_user_generic::<InMemoryUserRepo>)).route("/users", post(create_user_generic::<InMemoryUserRepo>)).with_state(AppStateGeneric { user_repo });let app = Router::new().nest("/dyn", using_dyn).nest("/generic", using_generic);let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();tracing::debug!("listening on {}", listener.local_addr().unwrap());axum::serve(listener, app).await.unwrap();
}#[derive(Clone)]
struct AppStateDyn {user_repo: Arc<dyn UserRepo>,
}#[derive(Clone)]
struct AppStateGeneric<T> {user_repo: T,
}#[derive(Debug, Serialize, Clone)]
struct User {id: Uuid,name: String,
}#[derive(Deserialize)]
struct UserParams {name: String,
}async fn create_user_dyn(State(state): State<AppStateDyn>,Json(params): Json<UserParams>,
) -> Json<User> {let user = User {id: Uuid::new_v4(),name: params.name,};state.user_repo.save_user(&user);Json(user)
}async fn get_user_dyn(State(state): State<AppStateDyn>,Path(id): Path<Uuid>,
) -> Result<Json<User>, StatusCode> {match state.user_repo.get_user(id) {Some(user) => Ok(Json(user)),None => Err(StatusCode::NOT_FOUND),}
}async fn create_user_generic<T>(State(state): State<AppStateGeneric<T>>,Json(params): Json<UserParams>,
) -> Json<User>
whereT: UserRepo,
{let user = User {id: Uuid::new_v4(),name: params.name,};state.user_repo.save_user(&user);Json(user)
}async fn get_user_generic<T>(State(state): State<AppStateGeneric<T>>,Path(id): Path<Uuid>,
) -> Result<Json<User>, StatusCode>
whereT: UserRepo,
{match state.user_repo.get_user(id) {Some(user) => Ok(Json(user)),None => Err(StatusCode::NOT_FOUND),}
}trait UserRepo: Send + Sync {fn get_user(&self, id: Uuid) -> Option<User>;fn save_user(&self, user: &User);
}#[derive(Debug, Clone, Default)]
struct InMemoryUserRepo {map: Arc<Mutex<HashMap<Uuid, User>>>,
}impl UserRepo for InMemoryUserRepo {fn get_user(&self, id: Uuid) -> Option<User> {self.map.lock().unwrap().get(&id).cloned()}fn save_user(&self, user: &User) {self.map.lock().unwrap().insert(user.id, user.clone());}
}
1使用 trait + 泛型

在編譯時,Rust 編譯器會為每個使用了該泛型函數的具體類型生成一個獨立的、優化的版本。這被稱為靜態分發,因為方法調用在編譯時就已經確定并硬編碼。

  1. 定義結構體AppStateGeneric<T> ,它的字段 user_repo 的類型是 T
  2. create_user_generic<T>get_user_generic<T> 函數,它們都是泛型函數,通過 where T: UserRepo 約束 T 必須實現 UserRepo trait。
  3. main 函數中,它們被實例化為 create_user_generic::<InMemoryUserRepo>get_user_generic::<InMemoryUserRepo>,這意味著編譯器會專門為 InMemoryUserRepo 生成一個版本的函數。

優點: 零成本抽象。由于方法調用在編譯時就已經確定,運行時沒有額外的開銷,性能和直接調用具體類型的方法一樣快。

缺點: 靈活性較差。所有使用該泛型函數的類型必須在編譯時確定。這可能導致生成的代碼量增加,因為編譯器會為每個具體類型生成一個獨立的函數副本。

2. 使用 trait + 動態分發

Arc + dyn 來實現動態分發,使用 dyn Trait(如 Arc<dyn UserRepo>)來存儲一個指向實現了 UserRepo trait 的任何具體類型的 trait 對象。在運行時,Rust 會通過虛函數表(vtable)來查找并調用正確的方法。這被稱為動態分發,因為方法調用是在運行時確定的。

  1. AppStateDyn 結構體:它的 user_repo 字段的類型是 Arc<dyn UserRepo>。這意味著它不關心具體是哪種 UserRepo 實現,只要它實現了 UserRepo trait 即可。

  2. create_user_dynget_user_dyn 函數:它們接受 AppStateDyn 作為狀態。方法調用如 state.user_repo.save_user(&user) 發生時,會動態地調用 InMemoryUserReposave_user 方法。

  3. 優點: 靈活性強。你可以在運行時切換不同的 UserRepo 實現,而不需要改變函數簽名。例如,你可以很容易地將 InMemoryUserRepo 換成 PostgresUserRepoRedisUserRepo,而這些 handler 函數(create_user_dyn 等)無需修改。

    缺點: 有輕微的性能開銷。因為需要在運行時通過 虛函數表(vtable )查找方法,這比直接調用具體類型的方法要慢一些。

為什么需要 Arc ?

在Rust 中ArcAtomic Rc 的縮寫,顧名思義:原子化的 Rc<T> 智能指針。Rust 所有權機制要求一個值只能有一個所有者,但是當遇到需要多個所有者時,Rust 巧妙的使用引用計數的方式,允許一個數據資源在同一時刻擁有多個所有者。這種實現機制就是 RcArc,前者適用于單線程,后者是原子化實現的引用計數,因此是線程安全的,可以用于多線程中共享數據。

總結

結合Rust 強大的類型機制和內存所有權機制,讓我們同樣可以在 Rust Axum 中使用依賴注入的模式,實現高性能的數據共享。靜態分發(泛型)和動態分發(dyn)在實踐中,這兩種模式并非非此即彼。你可以根據具體需求進行選擇:如果你的服務依賴非常穩定,且對性能要求嚴苛,請選擇泛型;如果你的應用需要更強的可擴展性和靈活性(比如在不同環境中切換數據庫連接),那么動態分發是更好的選擇。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/919944.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/919944.shtml
英文地址,請注明出處:http://en.pswp.cn/news/919944.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

syn與quote的使用——結構體轉create語句

前言 syn和quote的簡單使用——生成結構體-CSDN博客https://blog.csdn.net/qq_63401240/article/details/150609865?spm1001.2014.3001.5501 前面使用syn和quote&#xff0c;發現挺好玩的&#xff0c;感覺可以干很多事情&#xff0c;不愧是Rust中的宏。 宏分為聲明宏和過程…

集中式負載均衡 vs. 分布式負載均衡

集中式負載均衡 vs. 分布式負載均衡負載均衡&#xff08;Load Balancing&#xff09;是任何可伸縮系統的“交通警察”。 集中式負載均衡&#xff08;Centralized LB&#xff09;與分布式負載均衡&#xff08;Distributed LB&#xff09;代表了兩種截然不同的“指揮哲學”&#…

【機器學習】9 Generalized linear models and the exponential family

本章目錄 9 Generalized linear models and the exponential family 281 9.1 Introduction 281 9.2 The exponential family 281 9.2.1 Definition 282 9.2.2 Examples 282 9.2.3 Log partition function 284 9.2.4 MLE for the exponential family 286 9.2.5 Bayes for the e…

EndNote 2025 Mac 文獻管理工具

原文地址&#xff1a;EndNote 2025 Mac 文獻管理工具 EndNote mac版一款文獻管理工具&#xff0c;支持國際期刊的參考文獻格式有3776種&#xff0c;寫作模板幾百種&#xff0c;涵蓋各個領域的雜志。 EndNote mac不僅僅局限于投稿論文的寫作&#xff0c;對于研究生畢業論文的寫…

openEuler系統中home文件夾下huawei、HwHiAiUser、lost+found 文件夾的區別和作用

在 openEuler 系統的 /home 目錄下出現的 huawei、HwHiAiUser 和 lost+found 文件夾,分別對應不同的功能和用途,具體區別和作用如下: 1. lost+found 文件夾 通用 Linux 系統文件夾:lost+found 是所有 Linux 系統(包括 openEuler)中默認存在的文件夾,并非 openEuler 特有…

Electron 核心 API 全解析:從基礎到實戰場景

Electron 憑借豐富的 API 體系&#xff0c;讓前端開發者能輕松調用系統級能力。本文將系統梳理 Electron 核心 API 的分類、使用場景及實戰示例&#xff0c;幫你快速掌握從窗口管理到進程通信的全場景開發。 一、主進程核心 API&#xff08;Main Process&#xff09; 主進程是…

創建線程的方式有哪些?

1. 創建線程的方式有哪些?繼承Thread類實現runnable接口實現Callable接口線程池創建線程(項目中使用方式)2. runnable 和 callable 有什么區別?Runnable接口run方法沒有返回值Callable接口call方法有返回值,需要FutureTask獲取結果Callable接口的call()方法允許拋出異常;而Ru…

More Effective C++ 條款05: 謹慎定義類型轉換函數

More Effective C 條款05&#xff1a;謹慎定義類型轉換函數核心思想&#xff1a;C中的隱式類型轉換雖然方便&#xff0c;但容易導致意外的行為和維護難題。應當通過explicit關鍵字和命名轉換函數等方式嚴格控制類型轉換&#xff0c;優先使用顯式轉換而非隱式轉換。 &#x1f68…

基于springboot的理商管理平臺設計與實現、java/vue/mvc

基于springboot的理商管理平臺設計與實現、java/vue/mvc

Flask藍圖:模塊化開發的利器

藍圖為什么要使用藍圖模塊化組織&#xff1a;將應用分解為可重用的模塊&#xff08;組件&#xff09;。每個藍圖封裝了相關的視圖、靜態文件、模板等。按功能劃分&#xff1a;將大型應用按功能模塊劃分&#xff08;例如&#xff1a;用戶認證、博客、管理后臺&#xff09;&#…

設計模式詳解

1.創建類型1.1 簡單工廠startuml抽象產品接口 interface Product { Operation(): string } 具體產品A class ConcreteProductA { Operation(): string } 具體產品B class ConcreteProductB { Operation(): string } 工廠類 class Factory { CreateProduct(type: string): Produ…

前端查漏補缺

插槽默認、具名&#xff08;多個插槽&#xff09;、作用域&#xff08;接收子組件數據&#xff09;//具名 <div class"container"><header><slot name"header"></slot></header><main><slot></slot></…

網絡協議UDP、TCP

一、網絡協議 UDPUDP用戶數據報協議&#xff1a;傳輸層網絡編程模型B/S模型&#xff1a;browser/server&#xff08;瀏覽器/服務器&#xff09;客戶端是通用的客戶端&#xff08;瀏覽器&#xff09;一般只做服務器開發客戶端要加載的數據均來自服務器C/S模型&#xff1a;client…

STM32 TIM_SelectInputTrigger()函數

一、函數功能與定位?TIM_SelectInputTrigger()是STM32定時器外設的關鍵配置函數&#xff0c;用于設置從模式定時器的觸發源&#xff08;Trigger Source&#xff09;?。其核心作用是將定時器的內部事件或外部信號映射為觸發信號&#xff08;TRGI&#xff09;&#xff0c;進而控…

Lecture 6 Kernels, Triton 課程筆記

本講座&#xff1a;基準測試/分析 編寫內核 總結 編程模型&#xff08;PyTorch、Triton、PTX&#xff09;與硬件之間的差距 > 性能奧秘 理解擴展的基準測試 用于理解 PyTorch 函數內部結構的分析&#xff08;用內核觸底&#xff09; 看 PTX 匯編&#xff0c;了解 CUDA 內核…

Spring Boot 整合網易163郵箱發送郵件實現找回密碼功能

在開發用戶系統時&#xff0c;發送郵件是一項常見需求&#xff0c;例如用戶忘記密碼時&#xff0c;通過郵箱發送驗證碼來驗證身份并重置密碼。本文將結合 Spring Boot 和 163 郵箱&#xff0c;演示如何實現郵件發送功能。 一、前提條件 普通用戶的 163 郵箱可以在 Spring Boot…

如何在mac玩windows游戲?3個工具推薦,不用換電腦!

Mac電腦雖然很流暢&#xff0c;但它也存在局限性&#xff0c;其中一點游戲玩家應該深有體會&#xff0c;那就是無法直接玩Windows專屬游戲&#xff0c;只能對著琳瑯滿目的游戲望眼欲穿。別急&#xff0c;我有辦法讓你在mac玩windows游戲&#xff0c;下面就來分享我的經驗。一、…

自回歸(Auto-Regressive, AR),自回歸圖像生成過程

根據論文中“**T2I Generation via Next-Token Prediction**”一節&#xff0c;自回歸&#xff08;Auto-Regressive, AR&#xff09;文本到圖像&#xff08;T2I&#xff09;模型的圖像生成過程可分為三個主要步驟&#xff0c;其原理和損失函數如下&#xff1a;---### &#x1f…

在mysql中,modify ,change ,rename to的作用是什么

在 MySQL 中&#xff0c;MODIFY、CHANGE 和 RENAME TO 都是 ALTER TABLE 語句的一部分&#xff0c;用于修改表的結構&#xff0c;但它們的作用和使用場景有所不同。1. MODIFY作用&#xff1a;用于修改表中現有列的定義&#xff0c;但不能修改列名。你可以使用 MODIFY 來更改列的…

【JVM】JVM的內存結構是怎樣的?

JVM的內存結構是Java程序運行時內存管理的核心&#xff0c;不同區域有明確的職責。 一、整體劃分 包括兩大部分&#xff0c;分為線程私有區域(隨線程創建/銷毀&#xff0c;無需垃圾回收)和線程共享區域(所有線程共用&#xff0c;需要垃圾回收管理)。 線程私有區域&#xff1a;程…