Rust-AOP编程实战
文章本天成,妙手偶得之。粹然无疵瑕,岂复须人为?君看古彝器,巧拙两无施。汉最近先秦,固已殊淳漓。胡部何为者,豪竹杂哀丝。后夔不复作,千载谁与期?
——《文章》宋·陆游
【哲理】文章本是不加人工,天然而成的,是技艺高超的人在偶然间所得到的。其实作者所说的“天成”,并不就是大自然的恩赐,而是基于长期积累起来的感性印象和深入的思考,由于偶然出发而捕捉到灵感。
灵感就是长时间的积累和瞬间的爆发,人品也是,成就亦如是。
一、AOP 的概念
面向方面编程(Aspect-Oriented Programming,AOP)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)分离出来,从而提高代码的模块化和可维护性。横切关注点是指那些影响多个模块的功能,比如日志记录、安全检查、事务管理等。在传统的面向对象编程(OOP)中,这些关注点往往会散布在各个类中,导致代码重复和难以维护。
AOP 通过引入“方面”(aspect)的概念,将这些横切关注点集中到一个地方进行管理。主要的 AOP 概念包括:
- Aspect(方面):封装横切关注点的模块。
- Join Point(连接点):程序执行过程中可以插入方面的具体点,比如方法调用或异常抛出。
- Advice(通知):在特定的连接点上执行的代码,可以分为前置通知(Before)、后置通知(After)和环绕通知(Around)。
- Pointcut(切入点):定义在哪些连接点上应用通知的表达式。
- Weaving(织入):将方面应用到目标对象的过程,可以在编译时、加载时或运行时进行。
二、使用 Rust 实现 AOP
虽然 Rust 没有直接支持 AOP,但我们可以通过宏和闭包来实现类似的效果。
1、声明宏实现 AOP
1.1、定义宏和函数
首先,我们定义一个宏,用于在函数调用前后执行一些额外的逻辑:
macro_rules! aop {
($func:expr, $before:expr, $after:expr) => {{
$before();
let result = $func();
$after();
result
}};
}
这个宏接受三个参数:
$func
:要调用的函数。$before
:在函数调用前执行的闭包。$after
:在函数调用后执行的闭包。
1.2、使用宏实现 AOP
接下来,我们定义一些示例函数和通知,并使用 aop!
宏来包装函数调用:
fn main() {
// 定义前置通知
let before = || println!("Before function call");
// 定义后置通知
let after = || println!("After function call");
// 定义一个示例函数
let my_function = || {
println!("Inside the function");
42 // 返回一些值
};
// 使用 aop! 宏包装函数调用
let result = aop!(my_function, before, after);
println!("Function returned: {}", result);
}
运行这个程序,你会看到以下输出:
Before function call
Inside the function
After function call
Function returned: 42
1.3、环绕通知
为了更好地展示 AOP 的灵活性,我们可以扩展示例,添加更多的通知类型,比如环绕通知:
macro_rules! aop_around {
($func:expr, $around:expr) => {{
$around($func)
}};
}
fn main() {
// 定义环绕通知
let around = |func: fn() -> i32| {
println!("Before function call (around)");
let result = func();
println!("After function call (around)");
result
};
// 定义一个示例函数
let my_function = || {
println!("Inside the function");
42 // 返回一些值
};
// 使用 aop_around! 宏包装函数调用
let result = aop_around!(my_function, around);
println!("Function returned: {}", result);
}
运行这个扩展示例,你会看到以下输出:
Before function call (around)
Inside the function
After function call (around)
Function returned: 42
1.4、更精确的切入点定义
定义宏和函数
首先,我们定义一个宏,用于在函数调用前后执行一些额外的逻辑,并允许通过条件判断来决定是否应用通知:
macro_rules! aop {
($func:expr, $before:expr, $after:expr, $pointcut:expr) => {{
if $pointcut() {
$before();
}
let result = $func();
if $pointcut() {
$after();
}
result
}};
}
这个宏接受四个参数:
$func
:要调用的函数。$before
:在函数调用前执行的闭包。$after
:在函数调用后执行的闭包。$pointcut
:一个返回布尔值的闭包,用于决定是否应用通知。
使用宏实现更精确的切入点
接下来,我们定义一些示例函数和通知,并使用 aop!
宏来包装函数调用,同时定义切入点条件:
fn main() {
// 定义前置通知
let before = || println!("Before function call");
// 定义后置通知
let after = || println!("After function call");
// 定义一个示例函数
let my_function = || {
println!("Inside the function");
42 // 返回一些值
};
// 定义切入点条件
let pointcut = || true; // 可以根据需要修改条件
// 使用 aop! 宏包装函数调用
let result = aop!(my_function, before, after, pointcut);
println!("Function returned: {}", result);
}
运行这个程序,你会看到以下输出:
Before function call
Inside the function
After function call
Function returned: 42
如果我们修改切入点条件,使其返回 false
,则通知不会被应用:
fn main() {
// 定义前置通知
let before = || println!("Before function call");
// 定义后置通知
let after = || println!("After function call");
// 定义一个示例函数
let my_function = || {
println!("Inside the function");
42 // 返回一些值
};
// 定义切入点条件
let pointcut = || false; // 修改条件
// 使用 aop! 宏包装函数调用
let result = aop!(my_function, before, after, pointcut);
println!("Function returned: {}", result);
}
运行这个程序,你会看到以下输出:
Inside the function
Function returned: 42
扩展切入点条件
为了更灵活地定义切入点条件,我们可以将条件逻辑扩展为更加复杂的表达式。例如,我们可以根据函数名称、参数类型或其他上下文信息来决定是否应用通知。
fn main() {
// 定义前置通知
let before = || println!("Before function call");
// 定义后置通知
let after = || println!("After function call");
// 定义多个示例函数
let my_function1 = || {
println!("Inside function 1");
42 // 返回一些值
};
let my_function2 = || {
println!("Inside function 2");
24 // 返回一些值
};
// 定义切入点条件
let pointcut = |func_name: &str| func_name == "my_function1";
// 使用 aop! 宏包装函数调用
let result1 = aop!(my_function1, before, after, || pointcut("my_function1"));
println!("Function 1 returned: {}", result1);
let result2 = aop!(my_function2, before, after, || pointcut("my_function2"));
println!("Function 2 returned: {}", result2);
}
运行这个程序,你会看到以下输出:
Before function call
Inside function 1
After function call
Function 1 returned: 42
Inside function 2
Function 2 returned: 24
在这个示例中,只有 my_function1
满足切入点条件,因此只有它的调用前后会执行通知,而 my_function2
则不会。
1.5、简化切入点的定义过程
为了简化切入点的定义过程,我们可以通过封装和抽象来减少重复代码,并使切入点的定义更加直观和易于管理。以下是一些方法,可以帮助我们简化切入点的定义过程:
- 使用宏进行封装:将切入点逻辑封装在宏中,使其更易于复用。
- 使用函数指针或闭包:将切入点条件作为参数传递给宏,以便灵活地定义不同的切入点。
- 定义通用的切入点条件:创建一些常见的切入点条件函数,以便在不同场景中复用。
下面是一个示例,展示如何通过这些方法简化切入点的定义过程:
定义通用的切入点条件
首先,我们定义一些通用的切入点条件函数,这些函数可以根据需要进行扩展:
fn always_true() -> bool {
true
}
fn always_false() -> bool {
false
}
fn function_name_is(target: &str, func_name: &str) -> bool {
target == func_name
}
封装宏
接下来,我们定义一个宏,用于在函数调用前后执行通知,并接受切入点条件作为参数:
macro_rules! aop {
($func:expr, $before:expr, $after:expr, $pointcut:expr) => {{
if $pointcut() {
$before();
}
let result = $func();
if $pointcut() {
$after();
}
result
}};
}
使用宏和通用切入点条件
最后,我们使用这些通用的切入点条件和宏来包装函数调用:
fn main() {
// 定义前置通知
let before = || println!("Before function call");
// 定义后置通知
let after = || println!("After function call");
// 定义多个示例函数
let my_function1 = || {
println!("Inside function 1");
42 // 返回一些值
};
let my_function2 = || {
println!("Inside function 2");
24 // 返回一些值
};
// 使用 aop! 宏包装函数调用,并使用通用切入点条件
let result1 = aop!(my_function1, before, after, || function_name_is("my_function1", "my_function1"));
println!("Function 1 returned: {}", result1);
let result2 = aop!(my_function2, before, after, || function_name_is("my_function1", "my_function2"));
println!("Function 2 returned: {}", result2);
}
运行这个程序,你会看到以下输出:
Before function call
Inside function 1
After function call
Function 1 returned: 42
Inside function 2
Function 2 returned: 24
在这个示例中,我们使用了 function_name_is
函数来定义切入点条件,从而简化了切入点的定义过程。通过这种方式,我们可以轻松地复用通用的切入点条件,并且使代码更加简洁和易于维护。
2、过程宏实现 AOP
在 Rust 中,过程宏(procedural macros)是一种强大的工具,可以用来生成代码、修改代码结构以及实现复杂的编译时逻辑。通过使用过程宏属性,我们可以实现类似 AOP 的功能,并且使切入点的定义更加简洁和直观。
下面是一个示例,展示如何使用过程宏属性来实现 AOP 编程范例。
2.1、创建过程宏
首先,我们需要创建一个新的 Rust 库项目,用于定义我们的过程宏。你可以使用以下命令创建一个新的库项目:
cargo new aop_macro --lib
然后,在 Cargo.toml
文件中添加对 syn
和 quote
crate 的依赖:
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
[lib]
proc-macro = true
接下来,在 src/lib.rs
文件中定义我们的过程宏:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn aop(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let func_name = &input.sig.ident;
let block = &input.block;
// 解析前置和后置通知
let attr_args = attr.to_string();
let args: Vec<&str> = attr_args.split(',').collect();
let before = args.get(0).map(|s| s.trim()).unwrap_or("");
let after = args.get(1).map(|s| s.trim()).unwrap_or("");
let before_ident = syn::Ident::new(before, proc_macro2::Span::call_site());
let after_ident = syn::Ident::new(after, proc_macro2::Span::call_site());
let expanded = quote! {
fn #func_name() {
#before_ident();
let result = (|| #block)();
#after_ident();
result
}
};
TokenStream::from(expanded)
}
这个过程宏接受两个参数:前置通知和后置通知的函数名。它会在目标函数调用前后插入这些通知。
2.2、使用过程宏
接下来,我们在一个新的二进制项目中使用这个过程宏。你可以使用以下命令创建一个新的二进制项目:
cargo new aop_example
然后,在 aop_example
项目的 Cargo.toml
文件中添加对 aop_macro
库的依赖:
[dependencies]
aop_macro = { path = "../aop_macro" }
接下来,在 src/main.rs
文件中使用我们定义的过程宏:
use aop_macro::aop;
fn before() {
println!("Before function call");
}
fn after() {
println!("After function call");
}
#[aop(before, after)]
fn my_function() {
println!("Inside the function");
}
fn main() {
my_function();
}
运行这个程序,你会看到以下输出:
Before function call
Inside the function
After function call
通过使用过程宏属性,我们可以在 Rust 中实现类似 AOP 的功能,并且使切入点的定义更加简洁和直观。过程宏允许我们在编译时生成和修改代码,从而实现复杂的编译时逻辑。
2.3、优化过程宏的性能
优化过程宏的性能主要涉及减少编译时间和生成高效的代码。以下是一些优化过程宏性能的方法:
- 避免不必要的解析和转换。在编写过程宏时,尽量避免不必要的解析和转换操作。只解析和处理你需要的部分,以减少开销。
- 使用更高效的数据结构。在处理过程中,选择合适的数据结构可以提高效率。例如,使用
Vec
而不是HashMap
,如果你只需要顺序访问元素。 - 缓存结果。如果你的过程宏需要进行重复计算,可以考虑缓存中间结果以减少重复计算的开销。
- 减少依赖。尽量减少对外部 crate 的依赖,特别是那些会增加编译时间的依赖。对于必须使用的依赖,确保它们是最新版本,因为新版本通常包含性能改进。
- 优化生成的代码。确保生成的代码是高效的,不引入不必要的开销。例如,避免生成多余的闭包或函数调用。
- 使用
syn
和quote
的高级特性。syn
和quote
提供了许多高级特性,可以帮助你更高效地解析和生成代码。熟悉这些特性并加以利用,可以显著提高过程宏的性能。
三、与Spring Boot的AOP机制对比
Spring Boot 的 AOP(面向切面编程)机制和 Rust 中使用过程宏实现的 AOP 机制在概念上有相似之处,但在实现方式和应用场景上有显著的不同。以下是对这两种机制的详细对比:
1、实现方式
Spring Boot AOP:
- 基于代理:Spring AOP 主要通过动态代理(JDK 动态代理或 CGLIB 代理)来实现。这意味着 Spring AOP 在运行时生成代理对象,并在调用目标方法之前和之后插入通知逻辑。
- 注解驱动:Spring AOP 使用注解(如
@Aspect
、@Before
、@After
等)来定义切面和通知。开发者可以通过这些注解轻松地将横切关注点(如日志记录、事务管理等)应用到目标方法上。
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}
}
Rust 过程宏 AOP:
- 编译时代码生成:Rust 的过程宏在编译时生成代码。这意味着所有的切面逻辑在编译时就已经确定,不会在运行时引入额外的开销。
- 宏属性:通过自定义的宏属性,开发者可以在编译时插入通知逻辑。这个过程需要解析和修改抽象语法树(AST),然后生成新的代码。
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn aop(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let func_name = &input.sig.ident;
let block = &input.block;
// 解析前置和后置通知
let attr_args = attr.to_string();
let args: Vec<&str> = attr_args.split(',').collect();
let before = args.get(0).map(|s| s.trim()).unwrap_or("");
let after = args.get(1).map(|s| s.trim()).unwrap_or("");
let before_ident = syn::Ident::new(before, proc_macro2::Span::call_site());
let after_ident = syn::Ident::new(after, proc_macro2::Span::call_site());
let expanded = quote! {
fn #func_name() {
#before_ident();
let result = (|| #block)();
#after_ident();
result
}
};
TokenStream::from(expanded)
}
2、性能
Spring Boot AOP:
- 运行时开销:由于 Spring AOP 基于动态代理,在运行时可能会引入一些性能开销,特别是在创建代理对象和方法调用时。
- 灵活性:尽管有一定的运行时开销,Spring AOP 提供了极大的灵活性,可以在运行时动态地应用和移除切面。
Rust 过程宏 AOP:
- 编译时开销:Rust 的过程宏在编译时进行代码生成,因此不会在运行时引入额外的开销。编译时的处理可能会增加编译时间,但生成的代码在运行时非常高效。
- 静态性:由于所有的切面逻辑在编译时就已经确定,缺乏运行时的灵活性。这意味着无法在运行时动态地改变切面逻辑。
3、应用场景
Spring Boot AOP:
- 企业级应用:Spring AOP 广泛应用于企业级 Java 应用中,用于处理横切关注点,如事务管理、日志记录、安全性检查等。
- 动态配置:适用于需要在运行时动态配置和调整切面的场景。
Rust 过程宏 AOP:
- 系统编程:Rust 更适合系统编程和性能关键的应用场景。通过过程宏实现的 AOP 可以在不引入运行时开销的情况下实现类似的功能。
- 编译时保证:适用于需要在编译时确定所有逻辑的场景,提供更高的性能和安全性保证。
4、易用性
Spring Boot AOP:
- 易于使用:Spring AOP 提供了丰富的注解和配置选项,使得开发者可以轻松地定义和应用切面。
- 强大的生态系统:Spring 框架本身提供了大量的工具和库,与 AOP 紧密集成,进一步简化了开发过程。
Rust 过程宏 AOP:
- 学习曲线:编写过程宏需要深入了解 Rust 的宏系统和编译器插件,具有一定的学习曲线。
- 定制化:虽然过程宏提供了强大的功能,但需要开发者手动编写和维护宏代码,增加了复杂性。
5、总结
Spring Boot 的 AOP 机制和 Rust 中使用过程宏实现的 AOP 机制各有优劣。Spring AOP 提供了极大的灵活性和易用性,适用于企业级应用和动态配置的场景。而 Rust 的过程宏 AOP 则在性能和编译时保证方面具有优势,更适合系统编程和性能关键的应用。
四、AOP 实现记录日志(线程安全)
Step1、新增日志依赖
在 Cargo.toml
中添加 log
和 env_logger
依赖:
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
env_logger = "0.11.5"
log = "0.4.22"
[lib]
proc-macro = true
Step2、实现 aop 逻辑
修改过程宏文件 src/lib.rs
:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn aop(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let func_name = &input.sig.ident;
let fn_args = &input.sig.inputs;
let fn_return_type = &input.sig.output;
let block = &input.block;
// 解析前置和后置通知
let attr_args = attr.to_string();
let args: Vec<&str> = attr_args.split(',').collect();
let before = args.get(0).map(|s| s.trim()).unwrap_or("");
let after = args.get(1).map(|s| s.trim()).unwrap_or("");
let before_ident = syn::Ident::new(before, proc_macro2::Span::call_site());
let after_ident = syn::Ident::new(after, proc_macro2::Span::call_site());
let expanded = quote! {
fn #func_name(#fn_args) #fn_return_type {
log::info!("Before function call: {} with args: {:?}", stringify!(#func_name), (#fn_args));
#before_ident();
let result = (|| #block)();
#after_ident();
log::info!("After function call: {} returned: {:?}", stringify!(#func_name), result);
result
}
};
TokenStream::from(expanded)
}
Step3、使用 aop 过程宏
在 src/main.rs
中使用这个过程宏,并初始化日志记录器:
[dependencies]
aop_macro = { path = "../aop_macro" }
env_logger = "0.11.5"
log = "0.4.22"
use aop_macro::aop;
fn before() {
println!("Before function call");
}
fn after() {
println!("After function call");
}
#[aop(before, after)]
fn my_function() {
println!("Inside the function");
}
fn main() {
env_logger::init();
my_function();
}
通过设置 RUST_LOG
环境变量,你可以控制显示哪些日志信息。例如:
# RUST_LOG=info ./your_executable
RUST_LOG=info ./target/debug/aop_example
这将展示所有级别为 info
及其以上的日志条目。日志级别包括 error
、warn
、info
、debug
和 trace
,不区分大小写。
[2024-11-08T09:51:57Z INFO aop_example] Before function call: my_function with args: ()
Before function call
Inside the function
After function call
[2024-11-08T09:51:57Z INFO aop_example] After function call: my_function returned: ()
五、根据配置生成代码
我们可以编写一个过程宏来解析配置文件,并为指定的函数添加前置和后置操作。我们将通过以下步骤来实现这个功能:
- 定义一个配置文件,包含函数名、前置操作函数和后置操作函数。
- 编写一个过程宏,读取配置文件并为指定的函数添加前置和后置操作。
- 使用过程宏修饰目标函数。
Step1、创建宏
cargo new aop_config --lib
Cargo.toml 添加依赖项,
[dependencies]
env_logger = "0.11.5"
log = "0.4.22"
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
quote = "1"
proc-macro2 = "1"
syn = { version = "2", features = ["full"] }
[lib]
proc-macro = true
修改 src/lib.rs
:
extern crate proc_macro;
use chrono::Utc;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use std::fs;
use syn::{parse_macro_input, ItemFn};
#[derive(serde::Deserialize)]
struct Config {
func_name: String,
before: String,
after: String,
}
#[proc_macro_attribute]
pub fn aop(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_block = &input.block;
let fn_inputs = &input.sig.inputs;
let fn_output = &input.sig.output;
let arg_names = fn_inputs.iter().map(|arg| {
if let syn::FnArg::Typed(pat_type) = arg {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
return pat_ident.ident.to_string();
}
}
"".to_string()
});
// Read and parse the configuration file
let config_data = match fs::read_to_string("config.json") {
Ok(data) => data,
Err(_) => return TokenStream::from(quote! { #input }),
};
let configs: Vec<Config> = match serde_json::from_str(&config_data) {
Ok(configs) => configs,
Err(_) => return TokenStream::from(quote! { #input }),
};
// Find the matching configuration for the function
let config = configs.iter().find(|c| c.func_name == fn_name.to_string());
let expanded = if let Some(config) = config {
let before_fn = syn::Ident::new(&config.before, fn_name.span());
let after_fn = syn::Ident::new(&config.after, fn_name.span());
quote! {
fn #fn_name(#fn_inputs) #fn_output {
// before_fn
if !#before_fn() {
log::info!("Function {} skipped due to {}", stringify!(#fn_name), stringify!(#before_fn));
return Default::default();
}
// fn_block
//println!("Function {} called with args: {:?}", stringify!(#fn_name), (#(#arg_names),*));
let start_time = Utc::now();
log::info!("Before function call: {} with args: {:?}", stringify!(#fn_name), (#(#arg_names),*));
let result = (|| #fn_block)();
//println!("Function {} returned: {:?}", stringify!(#fn_name), result);
let end_time = Utc::now();
log::info!("After function call: {} returned: {:?}, elapsed time: {:?}", stringify!(#fn_name), result, end_time - start_time);
// after_fn
#after_fn();
result
}
}
} else {
quote! {
fn #fn_name(#fn_inputs) #fn_output {
//println!("Function {} called with args: {:?}", stringify!(#fn_name), (#(#arg_names),*));
let start_time = Utc::now();
log::info!("Before function call: {} with args: {:?}", stringify!(#fn_name), (#(#arg_names),*));
let result = (|| #fn_block)();
//println!("Function {} returned: {:?}", stringify!(#fn_name), result);
let end_time = Utc::now();
log::info!("After function call: {} returned: {:?}, elapsed time: {:?}", stringify!(#fn_name), result, end_time - start_time);
result
}
}
};
expanded.into()
}
Step2、使用宏
在 src/main.rs
中使用这个过程宏,并实现 fun_rule
和 fun_log
函数:
[dependencies]
aop_macro = { path = "../aop_macro" }
aop_config= { path = "../aop_config" }
env_logger = "0.11.5"
log = "0.4.22"
chrono = "0.4"
use aop_config::aop;
use env_logger::Builder;
use log::LevelFilter;
use std::collections::HashMap;
use chrono::Utc;
#[aop]
fn fun1(a:i32, b: i32, c: i32) -> (i32, i32, i32) {
println!("Inside fun1");
(a + 1, b + 1, c + 1)
}
#[aop]
fn fun2(d: i32, e: i32, f: i32) -> (i32, i32, i32) {
println!("Inside fun2");
(d * 2, e * 2, f * 2)
}
#[aop]
fn fun3(x: i32, y: i32, z: i32) -> (i32, i32, i32) {
println!("Inside fun3");
(x - 1, y - 1, z - 1)
}
fn fun_rule() -> bool {
// Define your rule here
true
}
fn fun_log() {
// Define your log operation here
println!("--------> Executing fun_log after function execution");
}
fn main() {
// Initialize logger
Builder::new().filter(None, LevelFilter::Info).init();
// Define the execution sequence
let sequence = vec![("fun1", vec![1, 2, 3]), ("fun2", vec![]), ("fun3", vec![])];
// Create a map of function pointers
let mut functions: HashMap<&str, Box<dyn Fn(Vec<i32>) -> Vec<i32>>> = HashMap::new();
functions.insert(
"fun1",
Box::new(|args| {
let (a, b, c) = (args[0], args[1], args[2]);
let (d, e, f) = fun1(a, b, c);
vec![d, e, f]
}),
);
functions.insert(
"fun2",
Box::new(|args| {
let (d, e, f) = (args[0], args[1], args[2]);
let (x, y, z) = fun2(d, e, f);
vec![x, y, z]
}),
);
functions.insert(
"fun3",
Box::new(|args| {
let (x, y, z) = (args[0], args[1], args[2]);
let (p, q, r) = fun3(x, y, z);
vec![p, q, r]
}),
);
// Execute the sequence
let mut current_args = sequence[0].1.clone();
for (func_name, _) in &sequence {
if let Some(func) = functions.get(func_name) {
current_args = func(current_args);
} else {
panic!("Function {} not found", func_name);
}
}
println!("Final result: {:?}", current_args);
}
Step3、配置文件
在主项目根目录下创建一个配置文件 config.json
,内容如下:
[
{
"func_name": "fun1",
"before": "fun_rule",
"after": "fun_log"
},
{
"func_name": "fun2",
"before": "fun_rule",
"after": "fun_log"
}
]
在这个示例中,我们定义了三个函数 fun1
, fun2
和 fun3
,并使用 AOP 宏来记录它们的参数值、返回值和耗时时间。我们还实现了一个简单的调度算法,根据预定义的执行顺序依次调用这些函数,并将每个函数的返回值作为下一个函数的参数。
此外,我们引入了 fun_rule
和 fun_log
函数。fun_rule
用于判断是否执行函数主体,如果不满足条件,则返回一个默认值。fun_log
用于在函数执行后进行额外的日志操作。
通过这种方式,我们实现了一个通用的函数调度算法,同时使用 AOP 记录每个函数的参数值、返回值和耗时时间,并根据配置条件决定是否执行某些函数。在多线程环境中,这种方法也是适用的,因为我们使用了线程安全的日志库 log
和 env_logger
。
Step4、运行效果
[2024-11-09T07:29:57Z INFO aop_example] Before function call: fun1 with args: ("a", "b", "c")
Inside fun1
[2024-11-09T07:29:57Z INFO aop_example] After function call: fun1 returned: (2, 3, 4), elapsed time: TimeDelta { secs: 0, nanos: 29902 }
--------> Executing fun_log after function execution
[2024-11-09T07:29:57Z INFO aop_example] Before function call: fun2 with args: ("d", "e", "f")
Inside fun2
[2024-11-09T07:29:57Z INFO aop_example] After function call: fun2 returned: (4, 6, 8), elapsed time: TimeDelta { secs: 0, nanos: 8210 }
--------> Executing fun_log after function execution
[2024-11-09T07:29:57Z INFO aop_example] Before function call: fun3 with args: ("x", "y", "z")
Inside fun3
[2024-11-09T07:29:57Z INFO aop_example] After function call: fun3 returned: (3, 5, 7), elapsed time: TimeDelta { secs: 0, nanos: 7769 }
Final result: [3, 5, 7]
Step5、源代码解析
我们可以通过 cargo-expand
查看编译器对代码进行宏展开后的结果,
cargo expand
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use aop_config::aop;
use env_logger::Builder;
use log::LevelFilter;
use std::collections::HashMap;
use chrono::Utc;
fn fun1(a: i32, b: i32, c: i32) -> (i32, i32, i32) {
if !fun_rule() {
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!("Function {0} skipped due to {1}", "fun1", "fun_rule"),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
return Default::default();
}
let start_time = Utc::now();
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!(
"Before function call: {0} with args: {1:?}",
"fun1",
("a", "b", "c"),
),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
let result = (|| {
{
::std::io::_print(format_args!("Inside fun1\n"));
};
(a + 1, b + 1, c + 1)
})();
let end_time = Utc::now();
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!(
"After function call: {0} returned: {1:?}, elapsed time: {2:?}",
"fun1",
result,
end_time - start_time,
),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
fun_log();
result
}
fn fun2(d: i32, e: i32, f: i32) -> (i32, i32, i32) {
if !fun_rule() {
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!("Function {0} skipped due to {1}", "fun2", "fun_rule"),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
return Default::default();
}
let start_time = Utc::now();
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!(
"Before function call: {0} with args: {1:?}",
"fun2",
("d", "e", "f"),
),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
let result = (|| {
{
::std::io::_print(format_args!("Inside fun2\n"));
};
(d * 2, e * 2, f * 2)
})();
let end_time = Utc::now();
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!(
"After function call: {0} returned: {1:?}, elapsed time: {2:?}",
"fun2",
result,
end_time - start_time,
),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
fun_log();
result
}
fn fun3(x: i32, y: i32, z: i32) -> (i32, i32, i32) {
let start_time = Utc::now();
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!(
"Before function call: {0} with args: {1:?}",
"fun3",
("x", "y", "z"),
),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
let result = (|| {
{
::std::io::_print(format_args!("Inside fun3\n"));
};
(x - 1, y - 1, z - 1)
})();
let end_time = Utc::now();
{
let lvl = ::log::Level::Info;
if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
::log::__private_api::log(
format_args!(
"After function call: {0} returned: {1:?}, elapsed time: {2:?}",
"fun3",
result,
end_time - start_time,
),
lvl,
&("aop_example", "aop_example", ::log::__private_api::loc()),
(),
);
}
};
result
}
fn fun_rule() -> bool {
true
}
fn fun_log() {
{
::std::io::_print(
format_args!("--------> Executing fun_log after function execution\n"),
);
};
}
总结
在这个简单的例子中,我们使用了配置文件来控制过程宏的行为。我们定义了一个 aop 过程宏作用于函数,而该过程宏的逻辑取决于 JSON 配置文件的定义,从而影响编译源码。过程宏是在编译时执行的,它们可以根据输入生成新的代码。以此类推,如果过程宏的行为依赖于系统变量,那么这些变量的值会直接影响生成的代码。