我认为这种效果来自ActionController::Reloader 的编写方式。这是来自 2.3.3 的 ActionController::Reloader#call,请注意评论:
def call(env)
Dispatcher.reload_application
status, headers, body = @app.call(env)
# We do not want to call 'cleanup_application' in an ensure block
# because the returned Rack response body may lazily generate its data. This
# is for example the case if one calls
#
# render :text => lambda { ... code here which refers to application models ... }
#
# in an ActionController.
#
# Instead, we will want to cleanup the application code after the request is
# completely finished. So we wrap the body in a BodyWrapper class so that
# when the Rack handler calls #close during the end of the request, we get to
# run our cleanup code.
[status, headers, BodyWrapper.new(body)]
end
Dispatcher.reload_application 不会删除自动加载的常量,Dispatcher.cleanup_application 会。 BodyWrapper#close 的编写考虑了可能的例外情况:
def close
@body.close if @body.respond_to?(:close)
ensure
Dispatcher.cleanup_application
end
但这并没有帮助,因为如果ActionController::Reloader#call 中的@app.call 抛出异常,BodyWrapper 不会被实例化,Dispatcher.cleanup_application 也不会被调用。
想象以下场景:
- 我对影响 API 调用的文件之一进行了更改
- 我点击 API 调用并看到错误,此时所有文件(包括有错误的文件)都没有卸载
- 我做了一个代码修复并点击相同的 API 调用来检查它是否有效
- 调用以与以前相同的方式路由到旧的类/对象/模块。这会引发相同的错误,并再次将加载的常量留在内存中
当传统控制器引发错误时不会发生这种情况,因为这些错误由 ActionController::Rescue 处理。此类异常不会命中ActionController::Reloader。
最简单的解决方案是将备用救援子句放入 API 路由中间件,这是一些变体:
def call(env)
# route API call
resuce Exception
Dispatcher.cleanup_application
raise
end
请注意,这是我对 3 岁问题的回答,我遵循了 2.3.3 的调用堆栈。较新版本的 rails 处理方式可能不同。