如何用Interceptor为GenServer拦截回调并添加自定义日志?
实现建议:无需修改业务代码的GenServer回调拦截方案
要实现仅通过替换use GenServer为use GenServerViz就能自动拦截所有GenServer回调(init/1、handle_call/3、handle_cast/2等)的需求,核心是利用Elixir的宏编译钩子,在用户模块编译阶段自动注入拦截逻辑,无需用户手动修改回调函数。以下是具体实现步骤:
1. 利用__using__宏注入编译钩子
在GenServerViz的__using__宏中,除了引入原生GenServer保证兼容性,还要为用户模块注册两个关键编译回调:
@on_definition:捕获用户模块中定义的所有函数,筛选出GenServer回调@before_compile:在模块编译收尾阶段,为捕获到的回调生成拦截逻辑
defmodule GenServerViz do defmacro __using__(opts) do # 允许用户自定义日志/API实现模块,默认用内置日志模块 log_module = Keyword.get(opts, :log_module, GenServerViz.DefaultLogger) quote do # 继承原生GenServer的所有功能 use GenServer, unquote(opts) # 存储用户指定的日志模块 @log_module unquote(log_module) # 注册回调捕获钩子 Module.register_attribute(__MODULE__, :target_callbacks, accumulate: true) # 捕获用户定义的函数,筛选GenServer回调 @on_definition fn _module, :def, name, args, _guards, _body -> target_names = [:init, :handle_call, :handle_cast, :handle_info, :terminate] if name in target_names do Module.put_attribute(__MODULE__, :target_callbacks, {name, length(args)}) end end # 编译收尾阶段生成拦截逻辑 @before_compile GenServerViz end end end
2. 在__before_compile__中自动生成拦截代码
通过Interceptor库或手动包裹逻辑,为每个捕获到的回调函数添加前后置处理。这里以Interceptor为例:
defmacro __before_compile__(env) do callbacks = Module.get_attribute(env.module, :target_callbacks) log_module = Module.get_attribute(env.module, :log_module) # 为每个回调生成拦截逻辑 Enum.map(callbacks, fn {name, arity} -> args = Macro.generate_arguments(arity, env.module) quote do # 用Interceptor包裹回调函数 Interceptor.intercept unquote(name)/unquote(arity), fn before: fn unquote_splicing(args) -> # 前置逻辑:调用用户指定的日志/API模块 unquote(log_module).before(unquote(name), unquote(args)) end, after: fn result, unquote_splicing(args) -> # 后置逻辑:记录结果,确保返回原结果不影响业务 unquote(log_module).after(unquote(name), unquote(args), result) result end end end end) end
3. 提供可扩展的日志/API实现模板
默认实现基础日志功能,同时允许用户通过log_module参数替换为自定义API调用:
# 默认日志实现 defmodule GenServerViz.DefaultLogger do def before(callback, args) do IO.puts("[GEN_SERVER_VIZ] #{callback} 执行前,参数:#{inspect(args)}") end def after(callback, args, result) do IO.puts("[GEN_SERVER_VIZ] #{callback} 执行后,结果:#{inspect(result)}") end end # 用户自定义API调用示例 defmodule MyAPILogger do def before(callback, args) do # 调用外部API记录请求 HTTPoison.post("https://api.example.com/gen_server/log", Jason.encode!(%{ event: "callback_before", callback: callback, args: args, timestamp: System.system_time(:millisecond) })) rescue e -> IO.puts("[API_LOG_ERROR] 前置记录失败:#{inspect(e)}") end def after(callback, args, result) do HTTPoison.post("https://api.example.com/gen_server/log", Jason.encode!(%{ event: "callback_after", callback: callback, args: args, result: result, timestamp: System.system_time(:millisecond) })) rescue e -> IO.puts("[API_LOG_ERROR] 后置记录失败:#{inspect(e)}") end end
4. 用户侧无侵入使用
用户只需替换use GenServer为use GenServerViz,可选指定自定义日志模块,无需修改任何回调代码:
defmodule MyBusinessServer do # 替换原生GenServer,可选指定自定义API日志 use GenServerViz, log_module: MyAPILogger def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__) @impl true def init(opts), do: {:ok, opts} @impl true def handle_call(:get_state, _from, state), do: {:reply, state, state} @impl true def handle_cast({:update, new_state}, _state), do: {:noreply, new_state} end
关键注意事项
- 兼容性:完全继承原生GenServer的所有功能,用户代码无需任何业务逻辑修改
- 错误隔离:日志/API逻辑用
try/rescue包裹,避免日志出错影响GenServer正常运行 - 覆盖所有回调:可根据需求扩展
target_names列表,拦截handle_info/2、terminate/2等其他GenServer回调
内容的提问来源于stack exchange,提问作者Astarno
相关产品推荐
相关产品推荐

