5. Interceptors
5.1. Overview
Interceptors are objects (or blocks) that sit between a client and a service and intercept messages (methods) sent to the service. Each service may have many such interceptors. When control is passed to an interceptor, it may then do something before and after passing control to the next interceptor, possibly even returning instead of passing control. This allows for some simple AOP-like hooks to be placed on your services.
Needle comes with one interceptor, the LoggingInterceptor. This allows you to easily trace the execution of your services by logging method entry and exit, as well as any exceptions that are raised.
You can, of course, implement your own interceptors as well.
5.2. Architecture
Interceptors are implemented as proxy objects that front the requested service. Thus, if you request a service that has 3 interceptors wrapped around it, you’re really getting a proxy object back that will invoke the interceptors (in order) before the requested method of the service is invoked.
The interceptors themselves are attached to the service during the execution of its instantiation pipeline (see Service Models). Thus, any action in the pipeline that is situated closer to the service than the interceptor pipeline action will bypass the interceptors altogether. This allows you to attach hooks to your service that can be called without invoking the interceptors on the service.
Another thing to keep in mind is that the interceptors are one-to-one for each service instance. Thus, if your service is a prototype (see the Service Models chapter), you’ll have one instance of each interceptor for each instance of your service.
5.3. Attaching
There are two ways to attach interceptors to your services. The first is to implement an interceptor factory that returns new interceptor instances, and attach the factory to your service. The second is to specify a block that implements the required functionality. Both have their uses.
Interceptor Factories
Interceptor factories are useful in situations where you want to implement some functionality and have it apply to multiple services. Interceptors from factories are also faster (less overhead) than interceptors from blocks, and so might be appropriate where performance is an issue.
An example is the LoggingInterceptor that ships with Needle. Because it is functionality that could be used on any number of services, it is implemented as a factory.
You can attach interceptor factories to your service using the #interceptor(...).with {...}
syntax:
1 2 | reg.register( :foo ) {...} reg.intercept( :foo ).with { MyInterceptorFactory } |
Note that you could also make the interceptor factory a service:
1 2 3 | reg.register( :foo ) {...} reg.register( :my_interceptor ) { MyInterceptorFactory } reg.intercept( :foo ).with { |c| c.my_interceptor } |
And, to make accessing interceptor services even more convenient, you can use the #with!
method (which executes its block within the context of the calling container):
1 2 3 | reg.register( :foo ) {...} reg.register( :my_interceptor ) { MyInterceptorFactory } reg.intercept( :foo ).with! { my_interceptor } |
Blocks
Sometimes creating an entire class to implement an interceptor is overkill. This is particularly the case during debugging or testing, when you might want to attach an interceptor to class to verify that a parameter passed is correct, or a return value is what you expect. To satisfy these conditions, you can using the
#doing
method. Just give it a block that accepts two parameters (the chain, and context) and you’re good to go!
1 2 | reg.register( :foo ) {...} reg.intercept( :foo ).doing { |chain,ctx| ...; chain.process_next( ctx ) } |
Note that this approach is about 40% slower than using an interceptor factory, so it should not be used if performance is an issue.
Options
Some interceptors can accept configuration options. For example, the LoggingInterceptor allows clients to specify methods that should and shouldn’t be intercepted. Options are specified via the #with_options
method.
1 2 3 4 | reg.register( :foo ) {...} reg.intercept( :foo ). with { |c| c.logging_interceptor }. with_options( :exclude => [ "method1", "method2" ] ) |
Options can apply to the blocks given to the #doing
method, too. The block may access the options via the #data[:options]
member of the context:
1 2 3 | reg.intercept( :foo ). doing { |ch,ctx| ...; p ctx.data[:options][:value]; ... }. with_options( :value => "hello" ) |
With blocks, of course, the value of such an approach is limited.
5.4. Ordering
As was mentioned, a service may have multiple interceptors attached to it. By default, a method will be filtered by the interceptors in the same order that they were attached, with the first interceptor that was attached being the first one to intercept every method call.
You can specify a different ordering of the interceptors by giving each one a priority. The priority is a number, where interceptors with a higher priority sort closer to the service, and those with lower priorities sort further from the service.
You can specify the priority as an option when attaching an interceptor:
1 2 3 | reg.register( :foo ) { ... } reg.intercept( :foo ).with { Something }.with_options( :priority => 100 ) reg.intercept( :foo ).with { SomethingElse }.with_options( :priority => 50 ) |
Without the priorities, when a method of :foo
was invoked, Something would be called first, and then SomethingElse. With the priorities (as specified), SomethingElse would be called before Something (since SomethingElse has a lower priority).
5.5. Custom
Creating your own interceptors is very easy. As was demonstrated earlier, you can always use blocks to implement an interceptor. However, for more complex interceptors, or for interceptors that you want to reuse across multiple services, you can also implement your own interceptor factories.
An interceptor factory can be any object, as long as it implements the method #new
with two parameters, the service point (service “definition”) of the service that the interceptor will be bound to, and a hash of the options that were passed to the interceptor when it was attached to the service. This method should then return a new interceptor instance, which must implement the #process
method. The #process
method should accept two parameters: an object representing the chain of interceptors, and the invocation context.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class MyInterceptorFactory def initialize( point, options ) ... end def process( chain, context ) # context.sym : the name of the method that was invoked # context.args : the array of arguments passed to the method # context.block : the block passed to the method, if any # context.data : a hash that may be used to share data between interceptors return context.process_next( context ) end end |
Once you’ve created your factory, you can attach it to a service:
reg.intercept( :foo ).with { MyInterceptorFactory }