如何在Actix-web中实现API函数重载,复用加密与非加密端点逻辑?
解决Actix-web中加密/非加密端点的代码重复问题
背景
我需要为API服务创建两类端点,分别支持生产环境的加密参数和开发环境的非加密参数,最终调用同一业务逻辑,例如:
/api/movie/get(生产环境,接收加密参数)/dev/movie/get(开发环境,接收非加密参数)
当前Actix-web实现
我用Actix-web的路由(非宏定义方式)配置路径路由,示例代码如下:
use actix_web::{web, App, HttpServer, Responder}; async fn index() -> impl Responder { "Hello world!" } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service( // 为所有附加的资源和路由添加前缀... web::scope("/app") // ...因此这个路由会处理`GET /app/index.html`请求 .route("/index.html", web::get().to(index)), ) }) .bind(("127.0.0.1", 8080))? .run() .await }
目前每个API都要写两个处理函数,分别对应加密和非加密请求:
/// 加密参数版本 /// 路径: /api/movie/get pub async fn handle_get_movie( app_data: Data<AppState>, Json(payload): Json<EncryptedPayload>, ) -> Result<impl Responder, Error> { // 解密内容 // 这是加密与非加密端点唯一的区别 let params = payload .unload::<GetMovieInf>() .map_err(|_| MainError::Malformatted)?; // 其他业务逻辑代码... Ok(Json(ReplyInf {ok: true})) }
/// 非加密参数版本 /// 路径: /dev/movie/get pub async fn handle_get_movie( app_data: Data<AppState>, Json(payload): Json<UnencryptedPayload>, ) -> Result<impl Responder, Error> { // 其他业务逻辑代码,和上面的函数完全一致... Ok(Json(ReplyInf {ok: true})) }
两个函数只有解密步骤不同,其余业务逻辑完全重复。
问题
能不能把这两个相似函数合并成一个?试过用泛型Json<T>但没成功,也考虑过通过环境变量控制启用EncryptedPayload还是UnencryptedPayload,想避免重复写代码。
解决方案
方案1:用Trait抽象参数提取逻辑
通过自定义Trait统一加密/非加密参数的解析行为,让泛型函数能兼容两种类型:
首先定义Trait,抽象出获取业务参数的方法:
use actix_web::Json; // 定义Trait,统一参数提取逻辑 trait ExtractParams<T> { fn extract_params(self) -> Result<T, MainError>; } // 为加密Payload实现Trait:解密并转换为业务参数 impl<T: serde::de::DeserializeOwned> ExtractParams<T> for EncryptedPayload { fn extract_params(self) -> Result<T, MainError> { self.unload::<T>().map_err(|_| MainError::Malformatted) } } // 为非加密Payload实现Trait:直接返回或转换为业务参数 // 假设UnencryptedPayload可以直接转为业务参数类型GetMovieInf impl ExtractParams<GetMovieInf> for UnencryptedPayload { fn extract_params(self) -> Result<GetMovieInf, MainError> { Ok(GetMovieInf::from(self)) // 如果UnencryptedPayload就是GetMovieInf本身,直接写Ok(self)即可 } }
然后编写泛型处理函数,复用业务逻辑:
pub async fn handle_get_movie<T, P>( app_data: Data<AppState>, Json(payload): Json<T>, ) -> Result<impl Responder, Error> where T: ExtractParams<P>, P: std::fmt::Debug, // 根据实际业务需求添加必要约束 { // 统一提取业务参数 let params = payload.extract_params()?; // 这里编写共享的业务逻辑 // ... Ok(Json(ReplyInf { ok: true })) }
最后在路由中注册两个端点,绑定到泛型函数的具体实例:
// 在App的路由配置里 App::new() .service( web::scope("/api") .route("/movie/get", web::post().to(handle_get_movie::<EncryptedPayload, GetMovieInf>)) ) .service( web::scope("/dev") .route("/movie/get", web::post().to(handle_get_movie::<UnencryptedPayload, GetMovieInf>)) );
方案2:环境变量控制中间件,统一处理解密
如果不需要同时运行两类端点(比如生产环境只启/api,开发环境只启/dev),可以用环境变量控制中间件的启用,让业务函数只处理一种参数:
- 定义解密中间件,生产环境自动解密请求体:
use actix_web::{dev::Service, middleware::Middleware, HttpRequest, HttpResponse, Error}; use futures_util::future::BoxFuture; struct DecryptMiddleware; impl<S, B> Middleware<S> for DecryptMiddleware where S: Service<Request = HttpRequest, Response = HttpResponse<B>, Error = Error>, S::Future: 'static, B: 'static, { type Service = DecryptService<S>; fn wrap(&self, service: S) -> Self::Service { DecryptService { inner: service } } } struct DecryptService<S> { inner: S, } impl<S, B> Service for DecryptService<S> where S: Service<Request = HttpRequest, Response = HttpResponse<B>, Error = Error>, S::Future: 'static, B: 'static, { type Request = HttpRequest; type Response = HttpResponse<B>; type Error = Error; type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&self, mut req: HttpRequest) -> Self::Future { // 这里实现读取请求体、解密、替换回请求体的逻辑 // 简化示例,实际需要处理请求体的异步读取和修改 // ... self.inner.call(req).boxed() } }
- 根据环境变量配置路由和中间件:
#[actix_web::main] async fn main() -> std::io::Result<()> { let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "dev".to_string()); let app = match run_mode.as_str() { "prod" => { // 生产环境:启用解密中间件,绑定/api前缀的路由 App::new() .wrap(DecryptMiddleware) .service( web::scope("/api") .route("/movie/get", web::post().to(handle_get_movie_unified)) ) } "dev" => { // 开发环境:直接绑定/dev前缀的路由,不用解密 App::new() .service( web::scope("/dev") .route("/movie/get", web::post().to(handle_get_movie_unified)) ) } _ => panic!("无效的RUN_MODE值:{}", run_mode), }; HttpServer::new(move || app.clone()) .bind(("127.0.0.1", 8080))? .run() .await }
这里的handle_get_movie_unified只需要处理非加密的业务参数GetMovieInf,中间件在生产环境自动完成解密,彻底避免了代码重复。
内容的提问来源于stack exchange,提问作者DennyHiu
相关产品推荐
相关产品推荐

