这两天在研究如何搭建一个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. 初始化代码
我们首先把项目结构规划好,先把相应的文件夹创建好:
初始化日志
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函数去调用,这样就能把整个模块初始化了。按照这个逻辑,其他的模块也这样实现模块初始化。
初始化数据库
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, }
初始化请求路由
我是把所有的请求路由全部放在了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
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
- 其实目前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。