Rails uses a series of initializers to initialize your application. While some of them might come from the gems you use, most of them are the Rails' built-in ones. And in some rare cases, you might need to customize some of them for special usages.
For example, I had to replace my app's set_routes_reloader_hook
initializer (the one that generates routes for your application) to benchmark our route generation logic. And it took a while to figure out how to do it the right way.
So if you don't need to do this, that's totally normal. But if you do, this post is for you.
The Steps
In this post, I'll use the set_routes_reloader_hook
initializer as our example. And the replacement will be done in 3 steps:
- Identify the Initializer's Source
- Register the New Initializer
- Drop the Old Initializer
And finally, I'll show you how to reuse the original initializer's body instead of copy&paste the code.
Identify the Initializer's Source
When talking about replacing a Rails initializer, your first thought might be modifying the Rails::Application#initializers
directly. But that won't work.
In Rails, Rails::Application#initializers
actually consists of 3 initializer sources:
def initializers #:nodoc:
Bootstrap.initializers_for(self) +
railties_initializers(super) +
Finisher.initializers_for(self)
end
So every time you call Rails::Application#initializers
, it returns a new array of initializers, and the change made on it won't be applied to the rest of the app.
What we need to do instead, is to identify which source that our target initializer belongs to:
Rails::Application::Bootstrap.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> false
Rails::Application.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> false
Rails::Application::Finisher.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> true
Now we can see that the set_routes_reloader_hook
is defined in Rails::Application::Finisher
.
Register the New Initializer
Because Rails executes initializers in the order they are defined, we need to make sure the new initializer is placed in the right place. So we need to use the target initializer as our index before dropping it. To do this, we can use the before
or after
argument when registering the new initializer:
# config/application.rb
require 'rails/all'
Rails::Application::Finisher.initializer :set_routes_reloader_hook_with_benchmark, after: :set_routes_reloader_hook do |app|
# ....
end
Drop the Old Initializer
And then the last step: drop the target initializer
# config/application.rb
require 'rails/all'
Rails::Application::Finisher.initializer :set_routes_reloader_hook_with_benchmark, after: :set_routes_reloader_hook do |app|
# ....
end
Rails::Application::Finisher.initializers.reject! { |i| i.name == :set_routes_reloader_hook }
Additional Tip: Reuse the Old Initializer
Since we're "replacing" the initializer instead of just dropping it or adding a new one, it means the new initializer may be very similar to the old one or just an extension of it. But because initializers aren't methods, we can't use super
like
def set_routes_reloader_hook
super
# other stuff you want to do
end
If this is bothering you, here's a trick: We can extract the old initializer's block, and use instance_eval
to evaluate it inside the new initializer
original_initializer_block = Rails::Application::Finisher.initializers.detect { |i| i.name == target_initializer }.block
Rails::Application::Finisher.initializer :new_initializer, after: target_initializer do |app|
instance_eval(&original_initializer_block)
# other stuff you want to do
end
Rails::Application::Finisher.initializers.reject! { |i| i.name == target_initializer }
Thank you for reading this post. If you like it, I also wrote some articles about debugging Rails applications/Ruby programs
Changing the Approach to Debugging in Ruby with TracePoint
Stan Lo for AppSignal ・ Apr 9 '20
Optimize Your Debugging Process With Object-Oriented Tracing and tapping_device
Stan Lo ・ Jan 21 '20
I'm also working on a gem called tapping_device. It can help you debug Ruby programs more easily by making objects tell you what they do.
Not sure what it means? There are examples in the readme 👇
Top comments (2)
what is the motivation of doing so? I don't see a clear advantage.
Hi thanks for the comment! I have just updated post intro accordingly!
My use case was to benchmark our app's route generation, so I had to replace the initializer to insert some benchmark logic.
But this post is not encouraging people to do so, just to provide a way to those who need to do it. Because it took me a while to do it right 😬