salvo搭建rust web项目

935 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

这两天在研究如何搭建一个rust web项目用来练习rust,目前正好搭了一个小demo,先记录下来。

感兴趣的小伙伴可以在github上下载,也欢迎小伙伴给一个小星星,谢谢!

1.初始化项目

首先我们使用cargo命令创建一个bin项目:

cargo new hello_salvo --bin

项目初始化成功后,我们使用idea导入。

2.添加依赖

项目初始化化,我们需要在Cargo.toml当中添加相应的依赖。

  • salvo依赖

    #web框架
    salvo = "0.27.0"
    tokio = { version = "1", features = ["macros"] }
    

    因为salvo依赖与tokio,所以想要的tokio我们也是需要导入的。salvo相当于java中的spring mvc的角色,承担了解析请求数据和写入响应数据的职责。它的底层通信是基于tokio实现的。

  • 序列化工具

    # 序列化工具
    serde = "1.0.140"
    

    serde是一个序列化工具,比如model转json字符串,json字符串转model。salvo里面类似的这种模型转换需要serde来提供。

  • 日志依赖

    #日志依赖
    tracing = "0.1.35"
    tracing-subscriber = "0.3.15"
    

    这个就是日志相关的依赖了,因为我们不能在项目当中到处写println,所以在项目初始化的时候就应该配置好日志处理。这个依赖的使用,以后也会花一些篇幅来讲解,这里暂时不细说。

  • 数据库依赖

    #数据库依赖
    mysql = "20.0.0"
    once_cell = "1.13.0"
    

    比较重要的就是数据库的依赖了,当前这个是mysql数据库的依赖。once_cell是用来初始化数据库连接池用的,它的作用是可以让对象初始化一次,相当于java当中的static作用,不排除还会在其他方面使用到它。

  • 时间处理工具

    #处理时间
    chrono = "0.4.19"
    

    chrono是为了接收从数据库中查询出时间类型的字段而引入进来的。不排除还会在其他方面使用到它。

3. 初始化代码

我们首先把项目结构规划好,先把相应的文件夹创建好: image-20220728203859324.png

  • 初始化日志 image-20220728204045126.png

    mod.rs

    use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
    ​
    pub fn init(){
        // 只有注册 subscriber 后, 才能在控制台上看到日志输出
        tracing_subscriber::registry().with(fmt::layer()).init();
    }
    

    在mod.rs中给一个init函数,我们在main函数去调用,这样就能把整个模块初始化了。按照这个逻辑,其他的模块也这样实现模块初始化。

  • 初始化数据库 image-20220728204340589.png

    mod.rs

    mod mysql_conn_pool;
    pub mod account_mapper;
    pub mod po;
    ​
    // const DB_URL: &str = "mysql://数据库用户名:数据库密码@数据库ip:数据库端口/数据库名称";pub fn init() {
        // 初始化链接池
        mysql_conn_pool::init_mysql_pool(DB_URL);
    }
    

    mysql_conn_pool.rs

    use mysql::{Pool, PooledConn};
    use once_cell::sync::OnceCell;
    use tracing::{instrument, info};
    ​
    // 创建一个全局的DB_POOL,可以一直使用,启动的时候初始化即可
    static DB_POOL: OnceCell<Pool> = OnceCell::new();
    ​
    // 初始化数据库链接池
    #[instrument]
    pub fn init_mysql_pool(db_url: &str) {
        info!("初始化数据库线程池--------开始-------");
        DB_POOL.set(mysql::Pool::new(&db_url).expect(&format!("Error connecting to {}", &db_url)))
            .unwrap_or_else(|_| { info!("try insert pool cell failure!") });
        info!("初始化数据库线程池--------结束-------");
    }
    ​
    // 从链接链接池里面获取链接
    #[instrument]
    pub fn get_connect() -> PooledConn {
        info!("从链接池获取数据库链接----------开始----------");
        let conn = DB_POOL.get().expect("Error get pool from OneCell<Pool>").get_conn().expect("Error get_connect from db pool");
        info!("从链接池获取数据库链接----------结束----------");
        conn
    }
    

    account_mapper.rs属于操作具体的数据表,类似于mybatis里面的Mapper作用。

    use mysql::prelude::{BinQuery, Queryable, WithParams};
    use mysql::params;
    use crate::dao::mysql_conn_pool::get_connect;
    use crate::dao::po::account::Account;
    use tracing::error;
    use std::error::Error;
    use tracing_subscriber::util::SubscriberInitExt;
    use uuid::Uuid;
    use crate::error::error::GlobalError;
    ​
    pub struct AccountMapper;
    ​
    impl AccountMapper {
        pub fn get_by_id(id: &str) -> Option<Account> {
            // 获取数据库链接
            let mut conn = get_connect();
            // 根据id查询账号信息
            let query_result = conn.exec_first("select id,account,password,enabled,create_time,modify_time from account where id=:id", params!("id"=>id))
                .map(|row| {
                    row.map(|(id, account, password, enabled, create_time, modify_time)| Account { id, account, password, enabled, create_time, modify_time })
                });
            // 判断是否查询到数据
            match query_result {
                Ok(result) => {
                    result
                }
                Err(_) => {
                    None
                }
            }
        }
    ​
        pub fn insert(account: &str, password: &str) -> Result<u64, GlobalError> {
            // 获取数据库链接
            let mut conn = get_connect();
            // 执行插入语句,目前id写死,后续会修改
            let x = match "insert into account (id,account,password,enabled,create_time,modify_time) values (?,?,?,1,now(),now())"
                .with(("123456", account, password))
                .run(&mut conn) {
                // 返回受影响的数据行数
                Ok(res) => {
                    Ok(res.affected_rows())
                }
                Err(e) => {
                    // error!(e);
                    Err(GlobalError::new("创建账号失败", e.to_string().as_str()))
                }
            };
            x
        }
    }
    ​
    

    account.rs

    use chrono::NaiveDateTime;
    use serde::Serialize;
    //这个结构体就和数据表一致。一共就六个字段。
    #[derive(Debug, PartialEq, Eq, Clone, Serialize)]
    pub struct Account {
        pub id: String,
        pub account: String,
        pub password: String,
        pub enabled: i32,
        pub create_time: NaiveDateTime,
        pub modify_time: NaiveDateTime,
    }
    
  • 初始化请求路由 image-20220728205338537.png

    我是把所有的请求路由全部放在了controller下面,所有路由的初始化也放在了里面。

    mod.rs

    use salvo::Router;
    use tracing::{instrument, info};
    ​
    mod user_controller;
    mod vo;
    ​
    // 按模块来初始化,这样就不用把所有的路由全部集中写到mod.rs里面了。
    #[instrument]
    pub fn init() -> Router {
        info!("收集所有的请求路由配置---------------开始---------------");
        let router = Router::new()
            .push(user_controller::init());
        info!(router=?router);
        info!("收集所有的请求路由配置---------------结束---------------");
        router
    }
    

    user_controller

    use salvo::prelude::{Request, Response, Router, handler, Json, Extractible};
    use salvo::http::header::{self, HeaderValue};
    use tracing::instrument;
    use crate::error::error::GlobalError;
    use crate::service::account_service::AccountService;
    use serde::{Serialize, Deserialize};
    ​
    // 在mod.rs初始化方法中被调用
    #[instrument]
    pub fn init() -> Router {
        Router::new()
            .push(Router::with_path("/user_info/<id>").get(get_user_info))
            .push(Router::with_hoop(add_header).get(hello_world))
            .push(Router::with_path("/create_account").post(create_account))
    }
    ​
    ​
    #[handler]
    async fn add_header(res: &mut Response) {
        res.headers_mut().insert(header::SERVER, HeaderValue::from_static("Salvo"));
    }
    ​
    #[handler]
    async fn hello_world() -> &'static str {
        "Hello World!"
    }
    ​
    #[handler]
    async fn get_user_info(req: &mut Request, res: &mut Response) {
        let option_id = req.param::<String>("id");
        match option_id {
            None => {
                panic!("id不合法");
            }
            Some(id) => {
                let account = AccountService::query_user_info_by_id(&id);
                res.render(Json(account));
            }
        }
    }
    ​
    #[handler]
    async fn create_account(user_info: UserInfo, res: &mut Response) -> Result<String, GlobalError> {
        let account = &user_info.account;
        let password = &user_info.password;
        match AccountService::add_account(account, password) {
            Ok(x) => {
                println!("受影响的行数:{}",x);
                Ok(String::from("成功!"))
            },
            Err(e) => Err(e)
        }
    }
    ​
    #[derive(Debug, Serialize, Deserialize, Extractible)]
    #[extract(
    default_source(from = "body", format = "json")
    )]
    struct UserInfo {
        pub account: String,
        pub password: String,
    }
    
  • 自定义error image-20220728205756712.png

    error.rs

    use salvo::{Depot, Request, Response, Writer, async_trait};
    use salvo::http::StatusCode;
    use salvo::prelude::Json;
    use serde::Serialize;
    ​
    #[derive(Debug, PartialEq, Eq, Clone, Serialize)]
    pub struct GlobalError {
        // 提示信息
        msg: String,
        // 错误信息
        error: String,
    }
    ​
    impl GlobalError {
        pub fn new(msg: &str, error: &str) -> GlobalError {
            GlobalError {
                msg: String::from(msg),
                error: String::from(error),
            }
        }
    }
    ​
    #[async_trait]
    impl Writer for GlobalError {
        async fn write(self, req: &mut Request, depot: &mut Depot, res: &mut Response) {
            res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
            res.render(Json(self));
        }
    }
    
  • 初始化service image-20220728210324724.png

    其实目前service我还没有啥要初始化的,所以mod.rs里面基本没写啥,等以后有需要再补充。

    mod.rs

    pub mod account_service;
    ​
    pub fn init(){
    ​
    }
    

    account_service.rs

    use crate::dao::po::account::Account;
    use crate::dao::account_mapper::AccountMapper;
    use core::fmt::Error;
    use crate::error::error::GlobalError;
    ​
    pub struct AccountService;
    ​
    impl AccountService {
        // 根据id查询账户信息
        pub fn query_user_info_by_id(id: &str) -> Account {
            // 校验参数
            if id.len() <= 0 {
                panic!("id不合法");
            }
            // 查询数据库
            let query_result = AccountMapper::get_by_id(id);
            // 验证查询结果
            match query_result {
                None => {
                    // 这里需要调整为自定义error,避免程序挂掉
                    panic!("没有查到任何数据");
                }
                Some(account) => {
                    account
                }
            }
        }
    ​
        // 添加账号
        pub fn add_account(account: &str, password: &str) -> Result<u64, GlobalError> {
            // 前面还要补充一些参数校验的过程
            AccountMapper::insert(account, password)
        }
    }
    

4.启动服务器

我把启动服务器的代码全部放在了main.rs里面

mod dao;
mod controller;
mod logs;
mod service;
mod error;
​
use salvo::prelude::{Server, TcpListener};
use tracing::{span, info, Level};
​
// 启动服务器
fn start_server(ip: &str, port: usize) -> Server<TcpListener> {
    Server::new(TcpListener::bind(&format!("{}:{}", ip, port)))
}
​
#[tokio::main]
async fn main() {
    // 初始化日志
    logs::init();
    let span = span!(Level::WARN, "main");
    let _enter = span.enter();
    info!("main function start");
    // 初始化数据库连接池
    dao::init();
    // 初始化service
    service::init();
    // 初始化请求路由
    let router = controller::init();
    // 启动服务
    start_server("127.0.0.1", 7878).serve(router).await;
}

至此,整个项目就算是初步完成了,我们就可以按照这个项目结构来编写我们的业务代码了。

后续会逐步补充我在这个项目当中遇到的问题,也欢迎小伙伴一起学习rust。