Java's MapStruct Implementiert in Rust
Olivia Novak
Dev Intern · Leapcell

Im Java-Ökosystem gibt es ein Bean-Konvertierungstool namens MapStruct, das die Konvertierung zwischen Beans sehr komfortabel macht. Sein Prinzip besteht darin, die Konvertierungsmethoden zur Kompilierzeit zu generieren. Da Rust-Makros auch das Generieren von Code zur Kompilierzeit unterstützen, habe ich beschlossen, eine einfache Version von MapStruct mithilfe von Attribut-Makros zu implementieren.
Makro-Grundlagen in Rust
Makros in Rust werden in zwei Hauptkategorien unterteilt: deklarative Makros (macro_rules!
) und drei Arten von prozeduralen Makros:
- Derive-Makros: Diese werden häufig verwendet, um spezifischen Code für Zielstrukturen oder -enums abzuleiten, wie z. B. das
Debug
-Trait. - Attributähnliche Makros: Diese werden verwendet, um Zielen benutzerdefinierte Attribute hinzuzufügen.
- Funktionsähnliche Makros: Diese ähneln Funktionsaufrufen.
Analyse der Implementierungsprinzipien
Wenn Sie in Rust zwischen Beans konvertieren möchten, ist dies recht einfach – Sie können das From
-Trait implementieren und die Konvertierungslogik innerhalb der from
-Methode definieren.
pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } impl From<Person> for PersonDto { fn from(item: Person) -> PersonDto { PersonDto { name: item.name, age: item.age, } } } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // Verwenden Sie die automatisch generierte From-Implementierung für die Konvertierung println!("dto: name:{}, age:{}", dto.name, dto.age); }
Um dies in Rust mithilfe von Makros zu implementieren, müssen wir also das Makro die From
-Methode automatisch generieren lassen, um so eine automatische Konvertierung zu ermöglichen.
Für eine einfache Verwendung habe ich mich von Diesels Syntax wie #[diesel(table_name = blog_users)]
inspirieren lassen. Unser Makro kann einfach durch Hinzufügen von #[auto_map(target = "PersonDto")]
über einer Struktur verwendet werden – sehr sauber und elegant.
#[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, }
Code-Implementierung
Da die Makro-Verwendung #[auto_map(target = "PersonDto")]
ist, ist der Makro-Workflow ungefähr festgelegt. Am Beispiel von Person
und PersonDto
sieht der Prozess wie folgt aus:
- Extrahieren Sie den Parameter
"target"
aus dem Makro. - Parsen Sie die Eingabestruktur (
Person
). - Extrahieren Sie die Feldnamen und -typen aus der Eingabestruktur.
- Parsen Sie den Zieltyp.
- Regenerieren Sie die ursprüngliche Struktur und implementieren Sie die
From
-Methode.
Schritt 1: Erstellen Sie das Projekt und fügen Sie Abhängigkeiten hinzu
cargo new rust_mapstruct --lib cd rust_mapstruct
Da die Makro-Code-Generierung das Parsen von Rusts AST erfordert, benötigen Sie zwei Schlüsselbibliotheken: quote
und syn
. Da wir Makros erstellen, müssen Sie außerdem proc-macro = true
angeben.
Vollständige Abhängigkeiten:
[lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0.17", features = ["full"] }
Schritt 2: Ändern Sie den Core-Code von lib.rs
1. Definieren Sie die Kernfunktion
#[proc_macro_attribute] pub fn auto_map(args: TokenStream, input: TokenStream) -> TokenStream { }
2. Extrahieren und Parsen Sie den Parameter "target"
Dies könnte erweitert werden, um mehrere Parameter zu unterstützen, aber da unser MapStruct-ähnliches Tool nur einen benötigt, gleichen wir direkt mit der Zeichenfolge target
ab. Sie können dies erweitern, um später weitere Parameter hinzuzufügen.
let args = parse_macro_input!(args as AttributeArgs); // Extrahieren und Parsen Sie den Parameter "target" let target_type = args .iter() .find_map(|arg| { if let NestedMeta::Meta(Meta::NameValue(m)) = arg { if m.path.is_ident("target") { if let Lit::Str(lit) = &m.lit { return Some(lit.value()); } } } None }) .expect("auto_map benötigt ein 'target'-Argument");
3. Parsen Sie die Eingabestruktur (Person
)
// Parsen Sie die Eingabestruktur let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; let struct_data = match input.data { Data::Struct(data) => data, _ => panic!("auto_map unterstützt nur Strukturen"), };
4. Extrahieren Sie Feldnamen und -typen aus Person
let (field_names, field_mappings): (Vec<_>, Vec<_>) = struct_data.fields.iter().map(|f| { let field_name = f.ident.as_ref().unwrap(); let field_type = &f.ty; (field_name.clone(), quote! { #field_name: #field_type }) }).unzip();
5. Parsen Sie den Zieltyp (PersonDto
)
syn::parse_str
kann eine Zeichenfolge in einen Rust-Typ konvertieren.
// Parsen Sie den Zieltyp let target_type_tokens = syn::parse_str::<syn::Type>(&target_type).unwrap();
6. Generieren Sie die ursprüngliche Struktur und die From
-Implementierung
Der Code in quote
fungiert als einfache Template-Engine. Wenn Sie schon einmal Templates für Webseiten geschrieben haben, sollte Ihnen dies bekannt vorkommen. Der erste Teil regeneriert die ursprüngliche Person
-Struktur, und der zweite Teil generiert die From
-Methode. Wir stecken die geparsten Parameter einfach in das Template.
// Regenerieren Sie die ursprüngliche Struktur und Konvertierungsimplementierung let expanded = quote! { // Hinweis: Dies generiert die ursprüngliche Struktur `Person` pub struct #struct_name { #( #field_mappings, )* } impl From<#struct_name> for #target_type_tokens { fn from(item: #struct_name) -> #target_type_tokens { #target_type_tokens { #( #field_names: item.#field_names, )* } } } }; expanded.into()
Schritt 3: Testen Sie das Makro in einem Projekt
Kompilieren Sie zuerst das Makroprojekt mit cargo build
. Erstellen Sie dann ein neues Testprojekt:
cargo new test-mapstruct cd test-mapstruct
Ändern Sie die Cargo.toml
-Abhängigkeiten
[dependencies] rust_mapstruct = { path = "../rust_mapstruct" }
Schreiben Sie einen einfachen Test in main.rs
use rust_mapstruct::auto_map; #[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // Verwenden Sie die automatisch generierte From-Implementierung für die Konvertierung println!("dto: name:{}, age:{}", dto.name, dto.age); }
Führen Sie den Code aus und sehen Sie sich das Ergebnis an
Führen Sie im Projekt test-mapstruct
cargo build
, cargo run
aus und sehen Sie sich das Ergebnis an!
❯ cargo build Compiling test-mapstruct v0.1.0 (/home/maocg/study/test-mapstruct) Finished dev [unoptimized + debuginfo] target(s) in 0.26s test-mapstruct on master ❯ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/test-mapstruct` dto: name:Alice, age:30
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Rust-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Auto-Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand - konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ