Rust Web 框架入门和对比

最适合使用的 Rust Web 框架

在 Web 开发的变化的浪潮中,Rust 已成为构建安全和高性能应用程序的首选语言。随着 Rust 的流行,一系列旨在利用其优势的 Web 框架也在不断出现。本文比较了一些最好的 Rust 框架,重点介绍了它们各自的优点和缺点,以帮助您为项目做出明智的决策。它还需要留心需要关注的框架,因为它们可能会改变我们在 Rust 中构建 Web 应用程序的方式。

由于大多数 Web 框架乍一看在使用上非常相似,但实际差异更加细微和琐碎。我希望强调文本中最重要的差异,但为了让您有更好的想法,我还展示了每个框架的示例代码,这些框架的作用不仅仅是简单的 hello world。所有示例均取自各自的 GitHub 存储库。

另请注意,这个列表绝不是详尽的,我肯定错过了一些现有的框架。如果您想包含您最喜欢的框架,请在 Twitter 或 Mastodon 上与我联系。

流行的 Rust 框架

Axum

Axum 是一个在 Rust 生态系统中具有特殊地位的 Web 应用程序框架。它是 Tokio 项目的一部分,该项目是使用 Rust 编写异步网络应用程序的运行时。 Axum 不仅使用 Tokio 作为其异步运行时,而且还与 Tokio 生态系统中的其他库集成,利用 Hyper 作为其 HTTP 服务器,并使用 Tower 作为中间件。这样,开发人员就能够重用 Tokio 生态系统中的现有库和工具。

Axum 还致力于在不依赖宏的情况下提供一流的开发人员体验,而是利用 Rust 的类型系统来提供安全且符合人体工程学的 API。这是通过使用特征来定义框架的核心抽象来实现的,例如 Handler 特征,它用于定义应用程序的核心逻辑。这种方法允许开发人员轻松地从较小的组件组​​成应用程序,这些组件可以在多个应用程序中重复使用。

Axum 中的处理程序是一个接受请求并返回响应的函数。这与其他后端框架类似,但利用 Axum 的 FromRequest trait,开发人员可以指定应从请求中提取的数据类型。返回类型需要实现 IntoResponse trait,并且已经有许多类型实现了此trait,包括允许轻松更改的元组类型,例如响应的状态代码。

如果您曾经使用过 Rust 的类型系统、泛型,尤其是trait中的异步方法(或更具体地说:返回的 Future ),您就会知道当您不满足 trait 约束(bound) 时,Rust 的错误消息会变得多么复杂。尤其是当您尝试适配 抽象 trait 约束(bound)时,经常会发生这样的情况:您会看到一堵难以破译的文本墙。改变几行的顺序,就没有任何作用了! Axum 提供了一个带有辅助宏的库,可以将错误定位到实际发生的位置,从而更容易理解出了什么问题。

Axum 做了很多正确的事情,并且很容易启动执行大量操作的应用程序。但是,您需要注意一些事项。该版本仍低于 1.0,Axum 团队可以从根本上更改版本之间的 API,这可能会导致您的应用程序崩溃。我们知道,这就是 0.x 版本的处理方式,但有些更改似乎非常微妙,但需要您开发一种不同的思维模型来了解底层的工作原理。如果您包含一个 Timeout 层(内置在 Tower 中),它在一个版本中可以轻松工作,在另一个版本中需要一个捕获所有错误处理程序,在下一个版本中需要一个绑定错误处理程序。这不是什么大问题,但当您尝试完成工作或使用最新版本启动项目并且事情突然发生变化时,可能会令人沮丧。此外,虽然您能够利用整个 Tokio 生态系统,但有时您需要处理粘合类型和特征,而不是直接使用 Tokio 函数。一个例子是使用任何与流和(网络)套接字相关的内容。

好的例子会有所帮助,但您需要持续跟踪。

尽管如此,Axum 是我个人最喜欢的,也是我用于 Shuttle Launchpad 的框架。我喜欢它的表现力和背后的概念,通过理解正确的概念,我想解决的问题都是我无法凭直觉完成的。如果您想了解 Axum 概念,请查看我的 Tokio + 微服务研讨会上的幻灯片。

Axum Example

Axum 仓库中的一个简短示例显示了一个 WebSocket 处理程序,该处理程序会回显它收到的任何消息。

rust 复制代码
#[tokio::main]
async fn main() {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app()).await.unwrap();
}

fn app() -> Router {
    // WebSocket routes can generally be tested in two ways:
    //
    // - Integration tests where you run the server and connect with a real WebSocket client.
    // - Unit tests where you mock the socket as some generic send/receive type
    //
    // Which version you pick is up to you. Generally we recommend the integration test version
    // unless your app has a lot of setup that makes it hard to run in a test.
    Router::new()
        .route("/integration-testable", get(integration_testable_handler))
        .route("/unit-testable", get(unit_testable_handler))
}

// A WebSocket handler that echos any message it receives.
//
// This one we'll be integration testing so it can be written in the regular way.
async fn integration_testable_handler(ws: WebSocketUpgrade) -> Response {
    ws.on_upgrade(integration_testable_handle_socket)
}

async fn integration_testable_handle_socket(mut socket: WebSocket) {
    while let Some(Ok(msg)) = socket.recv().await {
        if let Message::Text(msg) = msg {
            if socket
                .send(Message::Text(format!("You said: {msg}")))
                .await
                .is_err()
            {
                break;
            }
        }
    }
}

Axum总结

  • Macro-free API.
  • 利用 Tokio、Tower 和 Hyper 建立强大的生态系统。
  • 很棒的开发者经验。
  • 仍处于 0.x 版本,因此可能会发生重大更改。

Actix Web Actix

Actix Web 是 Rust 的 Web 框架之一,已经存在了一段时间,因此非常受欢迎。与任何优秀的开源项目一样,它经历了多次迭代,但它已经达到了 0 以上的主要版本,并保持了稳定性保证:在主要版本内,您可以确保没有重大更改。

当我们谈论 Actix Web 时,很容易假设它是基于 actix actor 运行时的。然而,这种情况已经有 4 年多没有发生了; Actix Web 中唯一需要 actor 的剩余部分是 WebSocket,但我们正在努力完全删除其使用,因为 actix 无法与现代异步 Rust 世界很好地配合。更广泛的 Actix 项目和 GitHub org 提供了许多用于构建并发应用程序的库,从较低级别的 TCP 服务器构建器,到 HTTP / Web 层,一直到静态文件提供程序和会话管理crate。

乍一看,Actix Web 看起来与 Rust 中的其他 Web 框架非常熟悉。您使用宏来定义 HTTP 方法和路由(如 Rocket),并使用提取器从请求中获取数据(如 Axum)。与 Axum 的相似之处是惊人的,在概念和特征的命名方式上也是如此。最大的区别是 Actix Web 并没有将自己与 Tokio 生态系统联系得太紧密。虽然 Tokio 仍然是 Actix Web 下的运行时,但该框架具有自己的抽象和特征,以及自己的crate生态系统。这有优点也有缺点。一方面,您可以确定事物通常可以很好地协同工作,另一方面,您可能会错过 Tokio 生态系统中已经提供的许多事物。

让我感到奇怪的一件事是 Actix Web 实现了自己的 Service trait,它与 Tower 的基本相同,但仍然不兼容。这意味着 Tower 生态系统中的大多数可用中间件不适用于 Actix。

同样有趣的是,如果您需要在 Actix Web 中自行实现一些特殊任务,您可能会遇到运行框架中所有内容的 Actor 模型。这可能会增加一些您可能不想处理的复杂性。

但 Actix Web 周围的社区提供了这一点。该框架支持 HTTP/2 和 Websocket 升级,它具有 Web 框架中最常见任务的包和指南,优秀的(我的意思是优秀的)文档,而且速度很快。 Actix Web 的流行是有原因的,如果您需要保持版本保证,它可能是您现在的最佳选择。

Actix Web Example

Actix Web 中的简单 WebSocket 回显服务器如下所示:

rust 复制代码
use actix::{Actor, StreamHandler};
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
use actix_web_actors::ws;

/// Define HTTP actor
struct MyWs;

impl Actor for MyWs {
    type Context = ws::WebsocketContext<Self>;
}

/// Handler for ws::Message message
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
            Ok(ws::Message::Text(text)) => ctx.text(text),
            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
            _ => (),
        }
    }
}

async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
    let resp = ws::start(MyWs {}, &req, stream);
    println!("{:?}", resp);
    resp
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().route("/ws/", web::get().to(index)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

Actix Web 总结

  • 强大、独立的生态系统。
  • Actor model based.
  • 通过主要版本保证实现稳定的 API。
  • 很棒的文档。

Rocket

Rocket 长期以来一直是 Rust Web 框架生态系统中的明星,它对开发人员体验的态度毫无歉意,对熟悉的现有概念的依赖,以及提供包含丰富内置功能的体验的雄心勃勃的目标。

当您进入他们漂亮的网站时,您可以看到它的野心:基于宏的路由、内置表单处理、对数据库和状态管理的支持,以及它自己的模板版本! Rocket 确实尝试完成构建 Web 应用程序所需的一切。

然而,Rocket的野心却付出了代价。虽然仍在积极开发中,但发布并不像以前那么频繁。这意味着该框架的用户会错过很多重要的东西。

此外,由于采用了包含丰富内置功能的方法,您还需要了解 Rocket 是如何工作的。 Rocket 应用程序有生命周期,构建块以特定方式连接,如果出现问题,您需要了解问题所在。

Rocket 是一个很棒的框架,如果你想开始 Rust Web 开发,它是一个不错的选择。就我个人而言,我对 Rocket 情有独钟,希望它的发展能够加快。对于我们许多人来说,Rocket 是第一个进入 Rust 的人,用它进行开发仍然很有趣。尽管如此,我通常依赖 Rocket 中不可用的功能,因此我不会在生产中使用它。

Rocket Example

Rocket 应用程序的一个简短示例,用于处理示例存储库中的表单:

rust 复制代码
#[derive(Debug, FromForm)]
struct Password<'v> {
    #[field(validate = len(6..))]
    #[field(validate = eq(self.second))]
    first: &'v str,
    #[field(validate = eq(self.first))]
    second: &'v str,
}

#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Submission<'v> {
    #[field(validate = len(1..))]
    title: &'v str,
    date: Date,
    #[field(validate = len(1..=250))]
    r#abstract: &'v str,
    #[field(validate = ext(ContentType::PDF))]
    file: TempFile<'v>,
    ready: bool,
}

#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Account<'v> {
    #[field(validate = len(1..))]
    name: &'v str,
    password: Password<'v>,
    #[field(validate = contains('@').or_else(msg!("invalid email address")))]
    email: &'v str,
}

#[derive(Debug, FromForm)]
#[allow(dead_code)]
struct Submit<'v> {
    account: Account<'v>,
    submission: Submission<'v>,
}

#[get("/")]
fn index() -> Template {
    Template::render("index", &Context::default())
}

// NOTE: We use `Contextual` here because we want to collect all submitted form
// fields to re-render forms with submitted values on error. If you have no such
// need, do not use `Contextual`. Use the equivalent of `Form<Submit<'_>>`.
#[post("/", data = "<form>")]
fn submit<'r>(form: Form<Contextual<'r, Submit<'r>>>) -> (Status, Template) {
    let template = match form.value {
        Some(ref submission) => {
            println!("submission: {:#?}", submission);
            Template::render("success", &form.context)
        }
        None => Template::render("index", &form.context),
    };

    (form.context.status(), template)
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index, submit])
        .attach(Template::fairing())
        .mount("/", FileServer::from(relative!("/static")))
}

Rocket 总结

  • 丰富的内置功能。
  • Great developer experience.
  • 不像以前那样积极发展。
  • 对于初学者来说仍然是一个不错的选择。

鲜为人知但仍然令人兴奋的 Rust 框架

Warp

hello,Warp!你是一头美丽、奇怪、强大的野兽。 Warp 是一个构建在 Tokio 之上的 Web 框架,它是一个非常好的框架。它也与我们迄今为止看到的其他框架有很大不同。

Warp 与 Axum 有一些共同traits(哈哈!):它基于 Tokio 和 Hyper 构建,并使用 Tower 中间件。然而,它的方法却非常不同。 Warp 是建立在 Filter trait之上的。

在 Warp 中,您构建了一个应用于传入请求的过滤器管道,并且请求通过管道传递,直到到达末尾。过滤器可以链接,也可以组合。这使您可以构建非常复杂但仍然易于理解的管道。

Warp 也比 Axum 更接近 Tokio 生态系统,这意味着您可以处理更多没有任何粘合特征的 Tokio 结构和概念。

Warp 采用非常实用的方法,如果这是您的编程风格,您一定会喜欢 Warp 的表现力和可组合性。当你查看一段 Warp 代码时,它通常读起来就像一个正在发生的故事,这在 Rust 中起作用是有趣和令人惊奇的。

不过,您可能想在 RRust Analyzer设置中关闭 类型嵌套 (inlay) 提示。由于所有这些不同的函数和过滤器都被链接在一起,Warp 中的 类型 变得非常长且非常复杂,而且也难以破译。错误消息也是如此,它可能是难以理解的一堆消息。

此外,虽然过滤器概念在您完成后很棒,但有时您希望拥有与所有其他框架一起获得的声明性路由器、处理程序和提取器样式。

Warp 是一个很棒的框架,我喜欢它。然而,它不是最适合初学者的框架,也不是最流行的框架。这意味着您可能很难找到帮助和资源。但对于快速且小型的应用程序来说,它很有趣,而且它的实验风格可能会给您带来新的想法!

Warp Example

来自示例存储库的 websocket 聊天的演示示例:

rust 复制代码
static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1);

/// Our state of currently connected users.
///
/// - Key is their id
/// - Value is a sender of `warp::ws::Message`
type Users = Arc<RwLock<HashMap<usize, mpsc::UnboundedSender<Message>>>>;

#[tokio::main]
async fn main() {
    let users = Users::default();
    // Turn our "state" into a new Filter...
    let users = warp::any().map(move || users.clone());

    // GET /chat -> websocket upgrade
    let chat = warp::path("chat")
        // The `ws()` filter will prepare Websocket handshake...
        .and(warp::ws())
        .and(users)
        .map(|ws: warp::ws::Ws, users| {
            // This will call our function if the handshake succeeds.
            ws.on_upgrade(move |socket| user_connected(socket, users))
        });

    // GET / -> index html
    let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML));

    let routes = index.or(chat);

    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}

async fn user_connected(ws: WebSocket, users: Users) {
    // Use a counter to assign a new unique ID for this user.
    let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed);

    eprintln!("new chat user: {}", my_id);

    // Split the socket into a sender and receive of messages.
    let (mut user_ws_tx, mut user_ws_rx) = ws.split();

    let (tx, rx) = mpsc::unbounded_channel();
    let mut rx = UnboundedReceiverStream::new(rx);

    tokio::task::spawn(async move {
        while let Some(message) = rx.next().await {
            user_ws_tx
                .send(message)
                .unwrap_or_else(|e| {
                    eprintln!("websocket send error: {}", e);
                })
                .await;
        }
    });

    // Save the sender in our list of connected users.
    users.write().await.insert(my_id, tx);

    // Return a `Future` that is basically a state machine managing
    // this specific user's connection.

    // Every time the user sends a message, broadcast it to
    // all other users...
    while let Some(result) = user_ws_rx.next().await {
        let msg = match result {
            Ok(msg) => msg,
            Err(e) => {
                eprintln!("websocket error(uid={}): {}", my_id, e);
                break;
            }
        };
        user_message(my_id, msg, &users).await;
    }

    // user_ws_rx stream will keep processing as long as the user stays
    // connected. Once they disconnect, then...
    user_disconnected(my_id, &users).await;
}

async fn user_message(my_id: usize, msg: Message, users: &Users) {
    // Skip any non-Text messages...
    let msg = if let Ok(s) = msg.to_str() {
        s
    } else {
        return;
    };

    let new_msg = format!("<User#{}>: {}", my_id, msg);

    // New message from this user, send it to everyone else (except same uid)...
    for (&uid, tx) in users.read().await.iter() {
        if my_id != uid {
            if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) {
                // The tx is disconnected, our `user_disconnected` code
                // should be happening in another task, nothing more to
                // do here.
            }
        }
    }
}

async fn user_disconnected(my_id: usize, users: &Users) {
    eprintln!("good bye user: {}", my_id);

    // Stream closed up, so remove from the user list
    users.write().await.remove(&my_id);
}

Warp 总结

  • Functional approach. 函数式方法。
  • Very expressive. 非常富有表现力。
  • 靠近 Tokio、Tower 和 Hyper,拥有强大的生态系统。
  • 不是最适合初学者的框架。

Tide

Tide 是一个非常简约的 Web 框架,构建在 async-std 运行时之上。简约的方法意味着您将获得非常小的 API 表面。 Tide 中的处理函数是 async fn ,它接受 Request 并返回 Responsetide::Result 。提取数据或发送正确的响应格式取决于您。

虽然这对您来说可能是更多的工作,但它也更直接,这意味着您可以完全控制正在发生的事情。对于某些情况,能够如此接近 HTTP 请求和响应是一件令人高兴的事情,并且使事情变得更容易。

它的中间件方法与您从 Tower 中了解到的类似,但 Tide 公开了异步特征箱,使实施变得更加容易。由于 Tide 是由也参与 Rust 异步生态系统的人们实现的,因此您可以期望像最近登陆 Nightly 的特征中的适当异步方法之类的东西很快就会被采用。

Tide Example

来自示例存储库的用户会话示例。

rust 复制代码
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    femme::start();
    let mut app = tide::new();
    app.with(tide::log::LogMiddleware::new());

    app.with(tide::sessions::SessionMiddleware::new(
        tide::sessions::MemoryStore::new(),
        std::env::var("TIDE_SECRET")
            .expect(
                "Please provide a TIDE_SECRET value of at \                      least 32 bytes in order to run this example",
            )
            .as_bytes(),
    ));

    app.with(tide::utils::Before(
        |mut request: tide::Request<()>| async move {
            let session = request.session_mut();
            let visits: usize = session.get("visits").unwrap_or_default();
            session.insert("visits", visits + 1).unwrap();
            request
        },
    ));

    app.at("/").get(|req: tide::Request<()>| async move {
        let visits: usize = req.session().get("visits").unwrap();
        Ok(format!("you have visited this website {} times", visits))
    });

    app.at("/reset")
        .get(|mut req: tide::Request<()>| async move {
            req.session_mut().destroy();
            Ok(tide::Redirect::new("/"))
        });

    app.listen("127.0.0.1:8080").await?;

    Ok(())
}

Tide 总结

  • Minimalistic approach. 简约的方法。
  • Uses async-std runtime. 使用 async-std 运行时。
  • Simple handler functions.
    简单的处理函数。
  • Playground of async features.

Poem

A program is like a poem, you cannot write a poem without writing it. --- Dijkstra

Poem 的自述文件用这些话向您致意。 Poem 声称是一个功能齐全且易于使用的 Web 框架。大胆的主张,但Poem似乎兑现了。乍一看,它的用法与 Axum 非常相似,唯一的区别是您需要使用相应的宏来标记处理函数。它还基于 Tokio 和 Hyper 构建,与 Tower 中间件完全兼容,同时仍然暴露自己的中间件特征。

Poem 的中间件特性也非常简单易用。您可以直接为所有或特定的 Endpoint 实现该特征(Poem 表达可以处理 HTTP 请求的所有内容的方式),或者您只编写一个接受 Endpoint 作为的异步函数范围。在与塔和服务特征打交道并有时挣扎了这么长时间之后,这是一股新鲜空气。

Poem 不仅与更广泛的生态系统中的许多功能兼容,而且它本身也充满了功能,包括对 OpenAPI 和 Swagger 文档的完全支持。而且它不仅限于基于HTTP的Web服务,还可以用于基于Tonic的gRPC服务,甚至可以用于Lambda函数,而无需切换框架。添加对 OpenTelemetry、Redis、Prometheus 等的支持,您可以勾选企业级应用程序的现代 Web 框架的所有框。

Poem 仍处于 0.x 版本,但如果它保持势头并提供可靠的 1.0,那么这是一个值得关注的框架!

Poem Example

来自示例存储库的 websocket 聊天的缩写版本:

rust 复制代码
#[handler]
fn ws(
    Path(name): Path<String>,
    ws: WebSocket,
    sender: Data<&tokio::sync::broadcast::Sender<String>>,
) -> impl IntoResponse {
    let sender = sender.clone();
    let mut receiver = sender.subscribe();
    ws.on_upgrade(move |socket| async move {
        let (mut sink, mut stream) = socket.split();

        tokio::spawn(async move {
            while let Some(Ok(msg)) = stream.next().await {
                if let Message::Text(text) = msg {
                    if sender.send(format!("{name}: {text}")).is_err() {
                        break;
                    }
                }
            }
        });

        tokio::spawn(async move {
            while let Ok(msg) = receiver.recv().await {
                if sink.send(Message::Text(msg)).await.is_err() {
                    break;
                }
            }
        });
    })
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let app = Route::new().at("/", get(index)).at(
        "/ws/:name",
        get(ws.data(tokio::sync::broadcast::channel::<String>(32).0)),
    );

    Server::new(TcpListener::bind("127.0.0.1:3000"))
        .run(app)
        .await
}

Poem 总结

  • 庞大的功能集。
  • 与 Tokio 生态系统兼容。
  • 便于使用。
  • 适用于 gRPC 和 Lambda。

需要关注的

Pavex

最初我说过所有 Rust Web 框架乍一看都非常相似。他们在细微差别上有所不同,有时甚至比其他人做得更好。

Pavex 是该规则的例外。目前,Pavex 的实施者正是 Luca Palmieri,他是广受欢迎的《零到生产》一书的作者。可以毫无疑问地说,Luca 知道他在做什么,他所有的想法和经验都融入到了 Pavex 中。

Pavex 有着显着的不同,因为它将自己视为构建 Rust API 的专用编译器。它需要对应用程序应该执行的操作进行高级描述,编译器会生成一个独立的 API Server SDK 包,可供配置和启动。

Pavex 仍处于早期阶段,但它绝对是一个值得关注的项目。查看 Luca 的博客文章了解更多信息。

总结

正如您所看到的,Rust Web 框架的世界非常多样化。没有一种万能的解决方案,您需要选择最适合您需求的框架。如果您刚刚开始,我建议您使用 Actix 或 Axum,因为它们是最适合初学者的框架,并且有很棒的文档。就我个人而言,我对 Pavex 将带来什么感兴趣,说实话,在成为 Axum 的长期用户之后,我真的很想看看 Poem。

原文地址:Best Rust Web Frameworks to Use in 2023

相关推荐
小蜗牛慢慢爬行3 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10431 小时前
java web springboot
java·spring boot·后端
龙少95432 小时前
【深入理解@EnableCaching】
java·后端·spring
溟洵4 小时前
Linux下学【MySQL】表中插入和查询的进阶操作(配实操图和SQL语句通俗易懂)
linux·运维·数据库·后端·sql·mysql
SomeB1oody7 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody7 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
啦啦右一8 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien8 小时前
Spring Boot常用注解
java·spring boot·后端
盛派网络小助手10 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
∝请叫*我简单先生11 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl