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:

Attaching an interceptor to a service [ruby]
1
2
reg.register( :foo ) {...}
reg.intercept( :foo ).with { MyInterceptorFactory }

Note that you could also make the interceptor factory a service:

Attaching an service as an interceptor [ruby]
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):

Attaching an service as an interceptor via #with! [ruby]
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!

Defining interceptors on the fly [ruby]
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.

Configuring interceptors [ruby]
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:

Configuring #doing interceptors [ruby]
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:

Setting interceptor priorities [ruby]
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.

Custom interceptor example [ruby]
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:

Attaching a custom interceptor [ruby]
reg.intercept( :foo ).with { MyInterceptorFactory }