为什么我要杀掉 syn:Rust 编译速度之战与 unsynn 的诞生

本文是对 The virtue of unsynn的整理与翻译


内容结构概览

markdown 复制代码
一、传言与真相:我真的想杀掉 serde 吗
    - facet 项目引发的 Reddit 讨论
    - 澄清:不是 serde,而是 syn
    - "free of syn" 运动的由来

二、syn 有多慢:用数字说话
    - M4 Pro 上的冷构建基准测试(facet vs syn@1 vs syn@2)
    - 单线程 -j1 模拟 CI 环境:3x vs 12x,差距扩大到约 4 倍
    - GitHub Actions 免费层实测数据

三、先搞清楚我们在比较什么:苹果 vs 核潜艇
    - syn 是什么:解析完整 Rust AST 的库
    - syn 能做什么:用一段代码解析自己
    - Rust 的声明式宏(macro_rules!)能做什么,边界在哪里

四、声明式宏 vs proc macro:从代码到构建时间
    - 用 print_fn_name 做对比示例
    - 声明式宏的局限性(pub fn 就能让它崩溃)
    - proc macro + syn 版本的实现
    - 宏展开结果对比:两者完全相同
    - 用 -Zunpretty=expanded 验证展开结果

五、构建时间差距有多大(微项目层面)
    - 冷构建:decl 111ms vs syn 1524ms,差 13.71 倍
    - 开启 opt-level=3 后:差距扩大到 36.76 倍
    - 两种常见反驳:proc macro 本来就慢 / 谁关心冷构建

六、不用 syn 也能写 proc macro:纯手工版
    - 直接操作 TokenStream 的完整实现代码
    - 性能:manual 205ms,仍比 syn 快 6.8 倍
    - 代价:人体工程学很差

七、unsynn:介于手工和 syn 之间的中间地带
    - unsynn 是什么(名字来自德语 unsinn,意为胡说八道)
    - keyword! 宏的用法与展开详情
    - unsynn! 宏中的核心组合子:Many / Cons / Except
    - 完整的 proc macro 实现代码
    - ToTokens 实现为什么需要手写
    - 为什么还需要 quote 和 proc_macro2(可测试性问题)

八、unsynn 的构建时间
    - 冷构建四路对比:decl 166ms / manual 267ms / unsynn 718ms / syn 1574ms
    - unsynn 比 syn 快 2.2 倍,比声明式宏慢 4.3 倍

九、热构建才是真正的战场
    - 用一万一千行 AI 生成的"垃圾代码"填满函数体
    - 热构建测试:decl≈197ms / manual≈204ms / unsynn≈208ms / syn≈304ms
    - syn 的真正问题:每次编译都要重新解析完整 AST,而 proc macro 调用结果不会被缓存
    - 100ms 被 syn 额外消耗,每次 cargo 认为有变化就要重付

十、宏观视角:syn 在真实项目里的影响
    - beardist 工具中 syn 出现 8 次的依赖树
    - 用因果剖析(coz)技术分析 syn 对关键路径的影响
    - jiff 不在关键路径上,使它更快没有意义
    - 魔法加速 syn 可以减少约 10% 的构建时间
    - 指向 facet 项目的背景

十一、小结

一、传言与真相

Reddit 上关于 facet(作者正在开发的 Rust 反射框架)的讨论线程里,开始流传一个传言:fasterthanlime 想要干掉 serde------Rust 社区最受欢迎的序列化/反序列化框架,为 Rust 的成功做出了巨大贡献。

作者的回应是:

那是绝对的、百分之百的真实。

然后他马上说:

开玩笑的。大部分是。

因为你没法真的"杀掉"一个 Rust crate。如果你扎它一下,它不会流血------作者对此深有体会,因为他正在积极尝试干掉的其实是另一个 crate:syn ,并为此发起了 free of syn 运动。


二、syn 到底有多慢

作者先拿出一组数据,但故意不解释在对比什么:

M4 Pro,并行构建(默认):

sql 复制代码
Benchmark 1: facet@0.x
  Time (mean ± σ):      1.241 s ±  0.003 s
  Range (min ... max):    1.236 s ...  1.244 s    10 runs

Benchmark 2: syn@2
  Time (mean ± σ):      2.679 s ±  0.035 s
  Range (min ... max):    2.643 s ...  2.742 s    10 runs

Benchmark 3: syn@1
  Time (mean ± σ):      2.885 s ±  0.077 s
  Range (min ... max):    2.780 s ...  3.001 s    10 runs

Summary
  facet@0.x ran
    2.16 ± 0.03 times faster than syn@2
    2.32 ± 0.06 times faster than syn@1

在 M4 Pro 这样的高端机器上,最慢的跑完不到三秒,看起来还好。

但如果用 -j1(单线程,模拟 CI 环境):

sql 复制代码
Benchmark 1: facet@0.x
  Time (mean ± σ):      3.225 s ±  0.035 s

Benchmark 2: syn@2
  Time (mean ± σ):     12.200 s ±  0.106 s

Benchmark 3: syn@1
  Time (mean ± σ):     12.162 s ±  0.028 s

Summary
  facet@0.x ran
    3.77 ± 0.04 times faster than syn@1
    3.78 ± 0.05 times faster than syn@2

12 秒。这段时间里,作者说他有时间泡好一杯红茶,思考人生。

GitHub Actions 免费层实测(2025 年 4 月 17 日):

sql 复制代码
Benchmark 1: facet@0.x
  Time (mean ± σ):      2.768 s ±  0.030 s

Benchmark 2: syn@2
  Time (mean ± σ):      9.352 s ±  0.039 s

Benchmark 3: syn@1
  Time (mean ± σ):      9.302 s ±  0.031 s

Summary
  facet@0.x ran
    3.36 ± 0.04 times faster than syn@1
    3.38 ± 0.04 times faster than syn@2

但等等,facetsyn 根本不是同一类东西,对比它们的构建时间意义是什么?


三、苹果与核潜艇

作者承认:facetsyn 不等价。就像在对比苹果和核潜艇。

syn 到底是什么?

syn 是一个 Rust 代码解析库,它可以把 Rust 源代码解析成完整的抽象语法树(AST)。

下面这段代码可以解析它自己:

rust 复制代码
fn main() {
    eprintln!("{:?}", syn::parse_file(include_str!("main.rs")).unwrap());
}

运行后输出的是整个文件的完整 AST 表示,包含每一个 token 的位置信息:

less 复制代码
File { shebang: None, attrs: [], items: [Item::Fn { attrs: [], vis: Visibility::Inherited,
sig: Signature { constness: None, asyncness: None, unsafety: None, ...

这在编写 proc macro(过程宏)时非常有用,因为你可以完整地访问和操作 Rust 代码的所有语法结构。


四、声明式宏能做什么,边界在哪里

Rust 有两种宏:声明式宏macro_rules!)和过程宏(proc macro)。

用声明式宏写一个"打印函数名"的宏:

rust 复制代码
macro_rules! print_fn_name {
    (fn $name:ident ($($args:tt),*) { $($body:tt)* }) => {
        fn $name($($args),*) {
            println!("Function name: {}", stringify!($name));
            $($body)*
        }
    };
}

print_fn_name! {
    fn main() {
        println!("Hello, world!")
    }
}

输出:

javascript 复制代码
Function name: main
Hello, world!

它在编译期操作 token,而不是像 C 的预处理器那样在文本层面工作。括号分组的概念是内置的,不用担心括号的内容。

但声明式宏的局限性马上出现------只要加一个 pub

rust 复制代码
print_fn_name! {
    pub fn main() {        // 这行有问题
        println!("Hello, world!")
    }
}
rust 复制代码
error: no rules expected keyword `pub`
  --> src/main.rs:11:5
note: while trying to match keyword `fn`

可以修复,在模式里加 $vis:vis

rust 复制代码
macro_rules! print_fn_name {
    ($vis:vis fn $name:ident ($($args:tt),*) { $($body:tt)* }) => {
        $vis fn $name($($args),*) {
            println!("Function name: {}", stringify!($name));
            $($body)*
        }
    };
}

但你会不断遇到下一个语法细节,不断修补。

用 syn 写 proc macro,就没有这个问题,它帮你解析完所有的 Rust 语法。


五、用 syn 写 proc macro

创建一个 proc macro crate:

bash 复制代码
cargo new --lib ouroboros-proc-macro

Cargo.toml 中添加:

toml 复制代码
[lib]
proc-macro = true

加入依赖 syn@2 -F fullquote,然后实现宏:

rust 复制代码
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_attribute]
pub fn print_fn_name(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input: syn::ItemFn = parse_macro_input!(item);
    let fn_vis = &input.vis;
    let fn_sig = &input.sig;
    let fn_name = &input.sig.ident;
    let fn_block = &input.block;

    let expanded = quote! {
        #fn_vis #fn_sig {
            println!("Function name: {}", stringify!(#fn_name));
            #fn_block
        }
    };
    TokenStream::from(expanded)
}

调用侧变得简洁:

rust 复制代码
use ouroboros_proc_macro::print_fn_name;

#[print_fn_name]
pub fn main() {
    println!("Hello, world!")
}

宏展开结果验证

-Zunpretty=expanded 可以查看两种宏的展开结果,它们完全相同

proc macro 版展开:

rust 复制代码
pub fn main() {
    {
        ::std::io::_print(format_args!("Function name: {0}\n", "main"));
    };
    {
        {
            ::std::io::_print(format_args!("Hello, world!\n"));
        }
    }
}

声明式宏版展开:

rust 复制代码
macro_rules! print_fn_name { /* ... */ }
pub fn main() {
    {
        ::std::io::_print(format_args!("Function name: {0}\n", "main"));
    };
    {
        ::std::io::_print(format_args!("Hello, world!\n"));
    }
}

唯一的区别是声明式宏版本还保留了宏的定义本身。

两者的本质区别不是功能,而是人体工程学syn 帮你解析了整个 Rust AST,你不用操心任何语法细节。


六、构建时间差距:冷构建(微项目)

先做基准测试,比较四种方案的冷构建时间:

less 复制代码
Benchmark 1: decl(声明式宏)
  Time (mean ± σ):     111.1 ms ±   2.1 ms

Benchmark 2: syn(proc macro + syn)
  Time (mean ± σ):      1.524 s ±  0.030 s

Summary
  decl ran
   13.71 ± 0.37 times faster than syn

声明式宏比 syn proc macro 快 13.71 倍

如果给 proc macro 开启优化(在 .config/cargo.toml 加):

toml 复制代码
[profile.dev.build-override]
opt-level = 3

结果是......更糟:

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     112.6 ms ±   2.1 ms

Benchmark 2: syn
  Time (mean ± σ):      4.140 s ±  0.070 s

Summary
  decl ran
   36.76 ± 0.93 times faster than syn

差距扩大到 36.76 倍 。优化的是 syn 本身的代码,让 syn 跑得更快了,但编译 syn 本身花的时间也更长了。


七、两种反驳与作者的回应

面对这些数据,文章预测了两种常见反驳:

反驳一:proc macro 本来就贵,这不是 syn 的问题。

作者的回应:不对,可以不用 syn 也能写 proc macro。见下节。

反驳二:谁关心冷构建?大家都做了缓存,热构建才是日常。

作者的回应:热构建同样有问题。见后续章节。


八、不用 syn 也能写 proc macro:纯手工版

直接操作 proc_macro 的原始 API:

rust 复制代码
// in `ouroboros-manual-macro/src/lib.rs`

use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};

#[proc_macro_attribute]
pub fn print_fn_name(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut tokens = item.into_iter();
    let mut output = Vec::new();

    // 1. 把 token 传过去,直到遇到 "fn"
    for token in &mut tokens {
        let is_fn = matches!(&token, TokenTree::Ident(ident) if ident.to_string() == "fn");
        output.push(token.clone());
        if is_fn {
            break;
        }
    }

    // 2. 下一个必须是函数名标识符
    let fn_name_ident = match tokens.next() {
        Some(TokenTree::Ident(ident)) => ident,
        _ => panic!("Expected function name after fn"),
    };
    let fn_name_str = fn_name_ident.to_string();
    output.push(TokenTree::Ident(fn_name_ident.clone()));

    // 3. 继续传,直到遇到函数体 { ... }
    for token in tokens {
        if let TokenTree::Group(group) = &token {
            if group.delimiter() == Delimiter::Brace {
                output.push(TokenTree::Group(Group::new(
                    Delimiter::Brace,
                    TokenStream::from_iter(
                        [
                            TokenTree::Ident(Ident::new("println", Span::call_site())),
                            TokenTree::Punct(Punct::new('!', Spacing::Alone)),
                            TokenTree::Group(Group::new(
                                Delimiter::Parenthesis,
                                TokenStream::from_iter([TokenTree::Literal(Literal::string(
                                    &format!("Function name: {fn_name_str}"),
                                ))]),
                            )),
                            TokenTree::Punct(Punct::new(';', Spacing::Alone)),
                        ]
                        .into_iter()
                        .chain(group.stream()),
                    ),
                )));
                continue;
            }
        }
        output.push(token);
    }

    output.into_iter().collect()
}

很繁琐,但能用,而且快:

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     108.5 ms ±   1.8 ms

Benchmark 2: manual(手工版 proc macro)
  Time (mean ± σ):     205.8 ms ±   1.3 ms

Benchmark 3: syn
  Time (mean ± σ):      1.500 s ±  0.035 s

Summary
  decl ran
    1.90 ± 0.03 times faster than manual
   13.82 ± 0.40 times faster than syn

手工版比声明式宏慢 1.9 倍,但比 syn 快 7.3 倍

问题是人体工程学太差了。如果只有这两个极端(声明式宏 vs 直接操作 TokenStream),选择会很难。

这就是 unsynn 出现的原因。


九、unsynn:介于手工和 syn 之间

unsynn 这个名字来自德语 "unsinn",意为"胡说八道/无稽之谈"。

它的思路很清晰:只解析你需要的部分,不解析你不需要的部分

第一步:定义关键字

rust 复制代码
use unsynn::*;

keyword! {
    KFn = "fn";
}

keyword! 是一个声明式宏,展开后生成:

rust 复制代码
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct KFn;

impl unsynn::Parser for KFn {
    fn parser(tokens: &mut unsynn::TokenIter) -> Result<Self> {
        use unsynn::Parse;
        unsynn::CachedIdent::parse_with(tokens, |ident, tokens| {
            if ident == "fn" {
                Ok(KFn)
            } else {
                unsynn::Error::other::<KFn>(
                    tokens,
                    format!(
                        "keyword {:?} expected, got {:?} at {:?}",
                        "fn",
                        ident.as_str(),
                        ident.span().start()
                    ),
                )
            }
        })
    }
}

impl unsynn::ToTokens for KFn {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        unsynn::Ident::new("fn", unsynn::Span::call_site()).to_tokens(tokens);
    }
}

impl AsRef<str> for KFn {
    fn as_ref(&self) -> &str {
        &"fn"
    }
}

第二步:声明要解析的结构

rust 复制代码
unsynn! {
    struct UntilFn {
        items: Many<Cons<Except<KFn>, TokenTree>>,
    }

    struct UntilBody {
        items: Many<Cons<Except<BraceGroup>, TokenTree>>,
    }

    struct Body {
        items: BraceGroup,
    }

    struct FunctionDecl {
        until_fn: UntilFn, _fn: KFn, name: Ident,
        until_body: UntilBody, body: Body
    }
}

核心组合子说明:

  • Many:一个或多个,类似正则表达式中的 +
  • Cons:这个,然后那个------两件事依次出现
  • Except:向前看,确保某个东西匹配。不消耗 token,只做检查
  • KFn:刚定义的 fn 关键字------不是字符串字面量,是真正的关键字 token
  • TokenTree:不只是单个 token,而是一整棵树。任何括号括起来的表达式都是一个 TokenTree

关键设计思路:我们不解析不需要解析的东西 。我们只是跳过 token,直到看到 fn 关键字,然后拿到标识符,跳过直到函数体,拿到函数体。

unsynn! 宏里:

  • struct = "一系列东西的序列"(类似带命名字段的 Cons<...>
  • enum = "替代选项"
  • Option<T> 也可以用

第三步:proc macro 入口

rust 复制代码
#[proc_macro_attribute]
pub fn print_fn_name(
    _attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item = TokenStream::from(item);
    let mut i = item.to_token_iter();
    let fdecl = i.parse::<FunctionDecl>().unwrap();

    let FunctionDecl {
        until_fn,
        _fn,
        name,
        until_body,
        body,
    } = fdecl;

    let fmt_string = format!("Function name: {}", name);
    quote::quote! {
        #until_fn fn #name #until_body {
            println!(#fmt_string);
            #body
        }
    }
    .into()
}

首先把输入 TokenStreamproc_macro 版本转换为 proc_macro2 版本(这是因为 proc_macro API 目前只对 proc macro crate 可用,如果想写单元测试需要抽象层),然后解析为 FunctionDecl

解构之后,用 quote! 把各个字段插值到生成的 token stream 中,同时保留 span 信息。

为了让 quote! 能插值,还需要为自定义类型实现 quote::ToTokens

rust 复制代码
impl quote::ToTokens for UntilFn {
    fn to_tokens(&self, tokens: &mut unsynn::TokenStream) {
        self.items.to_tokens(tokens)
    }
}

impl quote::ToTokens for UntilBody {
    fn to_tokens(&self, tokens: &mut unsynn::TokenStream) {
        self.items.to_tokens(tokens)
    }
}

impl quote::ToTokens for Body {
    fn to_tokens(&self, tokens: &mut unsynn::TokenStream) {
        tokens.extend(self.items.0.stream())
    }
}

基本上是把实现转发给已有的实现,因为 unsynn 自带 ToTokens trait,和 quote::ToTokens 本质上是同一个东西。

为什么还需要 quote 和 proc_macro2

unsynnquote 共享 proc_macro2 依赖,这是一种"必要之恶"。proc_macro API 目前还不能用于非 proc macro crate,所以如果想写单元测试,就需要某种抽象层。这个问题有 tracking issue 正在推进,但在文章写作时还有 PR 等待有人认领。


十、unsynn 的冷构建时间

四路对比:

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     166.2 ms ±   4.0 ms

Benchmark 2: manual
  Time (mean ± σ):     267.9 ms ±   3.0 ms

Benchmark 3: syn
  Time (mean ± σ):      1.574 s ±  0.004 s

Benchmark 4: unsynn
  Time (mean ± σ):     718.0 ms ±   1.9 ms

Summary
  decl ran
    1.61 ± 0.04 times faster than manual
    4.32 ± 0.10 times faster than unsynn
    9.47 ± 0.23 times faster than syn

unsynnsyn2.19 倍,比声明式宏慢 4.32 倍。

它比 syn 轻,也比 syn 做的事少------而后者本来就是重点。


十一、热构建才是真正的战场

很多人的反驳:"我不在乎冷构建,我有缓存,热构建才是日常。"

好,测热构建。

作者用 GPT-4.1 生成了大量嵌套的"垃圾代码",然后把同一个块复制 100 次,填满 main 函数体(约一万一千行代码)。

热构建的测量方式:touch main.rs,然后 cargo build。依赖本身不重新构建,但 proc macro 会重新处理整个函数体。

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     197.7 ms ±   3.6 ms

Benchmark 2: manual
  Time (mean ± σ):     204.0 ms ±   7.6 ms

Benchmark 3: syn
  Time (mean ± σ):     304.8 ms ±   3.1 ms

Benchmark 4: unsynn
  Time (mean ± σ):     208.6 ms ±   3.8 ms

Summary
  decl ran
    1.03 ± 0.04 times faster than manual
    1.05 ± 0.03 times faster than unsynn
    1.54 ± 0.03 times faster than syn

200ms 是解析编译这些代码的基本成本(假设声明式宏几乎免费)。调用一个已构建好的 proc macro 大约需要 10ms。syn 解析一万一千行"AI 垃圾代码"额外消耗了约 100ms

这才是作者不喜欢 syn 的真正原因

syn 不允许你"少做一点"。它总是解析完整的 Rust AST,即使你的需求远比这要简单。

而且,目前 proc macro 调用的结果不会被缓存 。只要 cargo 认为某个文件可能发生了变化,就会重新运行所有 proc macro。这意味着:任何一个使用了 syn 的 proc macro,每次 cargo 认为需要重新编译时,都要重新解析所有代码,生成所有代码,无论实际内容有没有变化。

这就是为什么我们需要 proc macro 尽可能少做工作,而不是把 syn 的完整 AST 解析带到每一次编译循环里。


十二、syn 在真实项目中的影响规模

微项目的测试说明了原理,但真实项目里 syn 的影响更大。

作者的内部工具 beardist 的依赖树里,syn 出现了 8 次

scss 复制代码
beardist on  main via Rust v1.86.0
❯ cargo tree -i syn --depth 1
syn v2.0.100
├── clap_derive v4.5.32 (proc-macro)
├── displaydoc v0.2.5 (proc-macro)
├── icu_provider_macros v1.5.0 (proc-macro)
├── serde_derive v1.0.219 (proc-macro)
├── synstructure v0.13.1
├── yoke-derive v0.7.5 (proc-macro)
├── zerofrom-derive v0.1.6 (proc-macro)
└── zerovec-derive v0.10.3 (proc-macro)

要把 syn 从这个依赖树里移除,意味着需要替换序列化框架、命令行解析、reqwest(通过 url crate 依赖 syn),工作量巨大。

用因果剖析(Causal Profiling)量化影响

直接测量 syn 的影响不容易,但可以用 coz 因果剖析器的技术来模拟:

  1. 先运行一次构建,其中每个 crate 的构建时间都人为加倍
  2. 再运行一次,其中除 syn 之外的每个 crate 构建时间加倍

两次构建之间的差异,就是"假如 syn 构建速度翻倍会带来多大改进"的虚拟加速。

这个技术来自因果剖析论文 COZ: Finding Code that Counts with Causal Profiling,核心思路是:通过人为减慢其他所有东西来模拟某个组件被加速的效果。

测试结果中几个有意思的发现:

  • jiff 虽然占了构建时间的相当一部分,但它不在关键路径上------让它更快对冷构建没有任何帮助,因为它会和其他事情并行完成
  • serde_derive 在这个特定项目里,即使魔法加速,也没有可测量的效果
  • syn 相关的 proc macro 依赖serde_deriveclap_derive 等)移到更早完成后,整体构建时间减少了约 10%

这是真实项目中 syn 对关键路径的量化影响。


十三、小结

这篇文章的完整论证链条:

ruby 复制代码
proc macro 使用 syn 解析 Rust 代码
  ↓
syn 解析完整 AST,即使你只需要其中很小的一部分
  ↓
proc macro 调用结果不被缓存
  ↓
每次 cargo 认为需要重新编译,syn 就要重新工作一遍
  ↓
在微项目上:冷构建慢 10--37 倍
  ↓
热构建:额外 100ms 每次
  ↓
在真实项目中:syn 在关键路径上,减少 10% 构建时间

替代方案的取舍对比:

方案 冷构建 热构建 人体工程学
声明式宏 最快(~111ms) 最快 受限,难以处理复杂语法
手工 proc macro 较快(~205ms) 很差,大量模板代码
unsynn 中等(~718ms) 较好,只解析需要的
syn 最慢(~1574ms) 最慢(+100ms) 最好,完整 AST 访问

作者的立场:

syn 是一个令人叹为观止的工具,它解析整个 Rust AST 的能力是真实的工程成就。但它不允许你"少做一点",而且 proc macro 缺乏缓存机制,让这个成本在每次编译中都要重新付出。

unsynn 是在"完整 AST 解析"和"直接操作原始 TokenStream"之间的中间地带:只描述你需要解析的结构,让其他部分透明地通过。

而 "free of syn" 运动,则是在推动整个 Rust 生态重新审视:当你只需要提取一个函数名、一个字段列表、一个枚举变体时,是否真的需要引入一个能解析整个 Rust 语言的完整解析器?


参考链接

相关推荐
星辰徐哥4 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥4 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约4 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee4 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐4 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs4 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐4 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司4 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
码农阿豪4 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端
追逐时光者4 小时前
一个基于 .NET 与 Avalonia 构建、面向 TrinityCore 的开源 WoW 数据库编辑器
后端·.net