You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 18:25:33