Skip to main content

Custom Injection Resolver Example

Custom Injection Resolution

This directory contains an example that illustrates how to write a custom injection resolver.

A custom injector allow users to define their own injection annotation, or to customize in some way the system injection resolver that does the Jakarta DI standard resolution. In this example, we will define our own injection resolver which customizes the JSR-300 standard one, but supplies the ability to get more information out of annotations on the parameter of a method.

In this use case, we want to have a method that can be injected, and which can annotate parameters of the method to determines the value that parameter should take. The real value will end up coming from an index into the data of an HttpRequest. Here is an example of a method that uses this custom injector, from the HttpEventReceiver class:

    @AlternateInject
    public void receiveRequest(
            @HttpParameter int rank,
            @HttpParameter(1) long id,
            @HttpParameter(2) String action,
            Logger logger) {
       //...
    }

The method receiveRequest takes the parameters rank, id, action and logger. But the determination of what value rank, id and action should take will be determined by the index in the HttpParameter annotation. Here is the definition of HttpParameter:

@Retention(RUNTIME)
@Target( { PARAMETER })
public @interface HttpParameter {
    /** The index  number of the parameter to retrieve */

   public int value() default 0;
}

The logger parameter of the receiveRequest method is just another service. This service will come from the normal Jakarta DI resolver, but the other parameters will be determined from the HttpParameter annotation. The determination of what the values should take comes from an object called the HttpRequest, which does nothing but store strings in certain indexes. The HttpRequest object itself is in the RequestScope context, which means its values will change depending on what request is currently active. In order to do that, the RequestScope context is a Proxiable context. We will see more about creating the RequestScoped context later in this document.

For now, lets look at how we define the @AlternateInject annotation. An injection annotation is valid on fields, methods, parameters of methods, constructors and parameters of constructors. However, in this case the @AlternateInject is only supported for methods, so the definition of @AlternateInject looks like this:

@Retention(RUNTIME)
@Target( { METHOD })
@InjectionPointIndicator
public @interface AlternateInject {
}

When providing a custom injection annotation, you must also provide an implementation of the InjectionResolver interface. It is this implementation that will be called whenever HK2 wants to inject into a constructor or field or method that is annotated with the custom injection annotation. The actual type of the parameterized type of the InjectionResolver implementation must be the custom injection annotation.

The annotation InjectionPointIndicator can optionally be placed on the custom annotation. Using this annotation allows the automatic analysis of a class with custom injection annotations prior to the registration of the associated InjectionResolver.

Here is how the AlternateInjectionResolver is defined:

@Singleton
public class AlternateInjectResolver implements InjectionResolver<AlternateInject> {
    //...
}

Implementations of InjectionResolver are registered with HK2 like any other service, and like any other service they may be injected with other services in the system. The AlternateInjectResolver is in the @Singleton context, which is the usual context for implementations of InjectionResolver. In general however implementations of InjectionResolver may be in any context. Implementations of InjectionResolver may not use the custom injection annotations that they themselves are defining to inject things into themselves.

Implementations of InjectionResolver that want to customize the default JSR-330 system provided injector can do so by injecting the default Jakarta DI system provided injector. The AlternateInjectionResolver does just that:

public class AlternateInjectResolver implements InjectionResolver<AlternateInject> {
    @Inject @Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
    private InjectionResolver<Inject> systemResolver;
}

The system Jakarta DI injection resolver is put into the registry with a specific name so that other injection resolvers can easily inject it using the @Named annotation.

Now we need to write the resolve method from InjectionResolver. We can get the current method we are injecting into from the passed in Injectee, and from that we can tell whether or not any particular parameter of the method has the @HttpParameter annotation. But what happens when a parameter does have the @HttpParameter annotation?

In that case, the real data should come from the underlying HttpRequest object. The HttpRequest object is a very simple object that stores strings at certain indexes:

@RequestScope
public class HttpRequest {
    public String getPathElement(int index) {...}

    public void addElement(String element) {...}
}

Because this is a request scoped object, the underlying values will change whenever the request has changed. So our AlternateInjectResolver can inject an HttpRequest object and use it to get values whenever it detects an @HttpParameter annotation on a parameter of the method. This is a code snippet from AlternateInjectResolver:

public class Foo {
    @Inject
    private HttpRequest request;

    public Object resolve(Injectee injectee, ServiceHandle<?> root) {
        //...

        Annotation annotations[] = method.getParameterAnnotations()[injectee.getPosition()];
        HttpParameter httpParam = getHttpParameter(annotations);
        if (httpParam == null) {
            return systemResolver.resolve(injectee, root);
        }

        int index = httpParam.value();
        String fromRequest = request.getPathElement(index);

        //...
    }
}

In the above code snippet the resolve method looks for an HttpParameter annotation on the particular parameter being injected. If it does not find such an annotation it simply lets the system injection resolver do the resolution. Otherwise, it gets the value from the injected HttpRequest.

But that is not the end of the story. The values that get injected into can be of type int, long or String. The resolve method can determine the type that is required to be returned, and ensure that it does the correct conversion before returning the object. Here is how that code works:

    Class<?> injecteeType = method.getParameterTypes()[injectee.getPosition()];
    if (int.class.equals(injecteeType)) {
        return Integer.parseInt(fromRequest);
    }
    if (long.class.equals(injecteeType)) {
        return Long.parseLong(fromRequest);
    }
    if (String.class.equals(injecteeType)) {
        return fromRequest;
    }

That is it for the implementation of our custom injection resolver! Every time the HttpEventReceiver class is instantiated its receiveRequest method will be called with the values from the current HttpRequest. The custom injection resolver was used to find the proper values in the HttpRequest and to convert them to the proper types. The logger would come from the default Jakarta DI resolver, since it is not annotated with the HttpParameter annotation.

The RequestScope Context

While the above is enough to demonstrate the custom injection resolver, it is instructive to also go through how the RequestScope context works.

The RequestScope context is a proxiable context that changes every time a new request has come in. The HttpRequest in the above example is in the RequestScope, and hence its underlying values will change whenever the request has been deemed to change.

In order to create such a scope/context, we first define the scope annotation, RequestScope:

@Scope
@Proxiable
@Retention(RUNTIME)
@Target( { TYPE, METHOD })
public @interface RequestScope {
}

The context that goes along with that request scope must implement the Context interface. The actual type of the Context parameterized type must be the annotation of the scope. The Context implementation for our RequestScope is called RequestContext and is defined like this:

@Singleton
public class RequestContext implements Context<RequestScope> {
    //...
}

Most implementations of Context are put into the Singleton scope, though this is not required. An implementation of Context are just like regular HK2 services, and so can be injected with other HK2 services.

The job of an implementation of Context is to keep a mapping of objects created for that particular context while that context is active. The code that looks up and finds objects for a particular request is straight-forward:

public class Bar {
    private final HashMap<ActiveDescriptor<?>, Object> requestScopedEntities = new HashMap<ActiveDescriptor<?>, Object>();

    public <U> U findOrCreate(ActiveDescriptor<U> activeDescriptor, ServiceHandle<?> root) {
        U retVal = (U) requestScopedEntities.get(activeDescriptor);
        if (retVal != null) {
            return retVal;
        }

        retVal = activeDescriptor.create(root);
        requestScopedEntities.put(activeDescriptor, retVal);

        return retVal;
    }

    public <U> U find(ActiveDescriptor<U> descriptor) {
        return (U) requestScopedEntities.get(descriptor);
    }
}

Since an implementation of Context is a service, it can be looked up by other services. RequestContext has methods on it that allow some controller to tell it when a request has started, and when it ends. When a request ends its objects are no longer needed and should be destroyed. Here are the methods on RequestContext that begin and end a request:

    private boolean inRequest = false;

    /**
     * Starts a request
     */
    public void startRequest() {
        inRequest = true;
    }

    public void stopRequest() {
        inRequest = false;

        for (Map.Entry<ActiveDescriptor<?>, Object> entry : requestScopedEntities.entrySet()) {
            ActiveDescriptor<Object> ad = (ActiveDescriptor<Object>) entry.getKey();
            Object value = entry.getValue();

            ad.dispose(value);
        }

        requestScopedEntities.clear();
    }

    public boolean isActive() {
        return inRequest;
    }

The startRequest method above sets the flag saying that the Request has begun. Any new requests to find or create request scoped objects will be adding those objects to the requestScopedEntities map. The stopRequest method above will set the flag saying that the request is over and destroy any objects that were created for this request. It then also clears the requestScopedEntities map so that this RequestScoped context is now clean and ready for the next request to come along. The isActive method of Context will tell the system whether or not there is a request that is active.

That is it for the implementation of our RequestScope scope/context pair. In this example the HttpRequest object is in the RequestScope, but this implementation does not preclude other services from also being in this scope. The scope is Proxiable, so that it can be injected into other objects with a different lifecycle (like the AlternateInjectResolver itself). Further, it properly disposes all request scoped objects that were created when the request has terminated.

Putting it all together

We now have a custom injection resolver and a custom scope. Lets look at the other classes in the example, to see how they tie everything together.

First we have a class called the HttpServer. The HttpServer is a mock HttpServer that takes requests from the faux network and does the following things:

  • Tells the RequestScope that a Request has begun
  • Fills in the HttpRequest with information from the wire

The request processing then continues from there, until the faux network decides that the request has finished. The HttpServer will then tell the RequestContext that the request has terminated.

Here is the implementation of our mocked HttpServer:

@Singleton
public class HttpServer {
    @Inject
    private HttpRequest httpRequest;

    @Inject
    private RequestContext requestContext;

    public void startRequest(String lastRank, String id, String action) {
        requestContext.startRequest();

        httpRequest.addElement(lastRank);
        httpRequest.addElement(id);
        httpRequest.addElement(action);
    }

    public void finishRequest() {
        requestContext.stopRequest();
    }
}

This mock HttpServer will be used by the test code to give the server requests from the network and to then end those requests. The injected HttpRequest will be created anew in the HttpServer.startRequest method when the RequestContext.startRequest() method is called.

We then have another class called RequestProcessor which is in the PerLookup scope and which is responsible for further handling the request. In our example it doesn’t have much to do other than injecting an instance of the HttpEventReceiver. Since the HttpEventReceiver is also in the PerLookup scope, it will be created whenever the instance of RequestProcessor is created. Here is the code for RequestProcessor:

@PerLookup
public class RequestProcessor {
    @Inject
    private HttpEventReceiver eventReciever;

    public HttpEventReceiver processHttpRequest() {
        return eventReciever;
    }
}

We can now look at how the test code work. The test has a helper method that does the following:

  • Gets the HttpServer service
  • starts a request by giving it passed in strings that came from the faux network
  • Gets a RequestProcessor service
  • Gets the HttpEventReceiver from the RequestProcessor
  • ends the request with the HttpServer
  • Checks that the values from the HttpEventReceiver were passed into it properly

Here is the test utility method:

    private void doRequest(int rank, long id, String event) {
        HttpServer httpServer = locator.getService(HttpServer.class);

        httpServer.startRequest("" + rank, "" + id, event);

        RequestProcessor processor = locator.getService(RequestProcessor.class);

        HttpEventReceiver receiver = processor.processHttpRequest();

        httpServer.finishRequest();

        // And now test that we got what we should have
        Assert.assertEquals(rank, receiver.getLastRank());
        Assert.assertEquals(id, receiver.getLastId());
        Assert.assertEquals(event, receiver.getLastAction());
    }

After having this utility method, the test itself is very simple, and just ensures that the whole thing fits together nicely:

    @Test
    public void testSomeRequests() {
        doRequest(50, 1, "FirstRequest");
        doRequest(100, 2, "SecondRequest");
        doRequest(1000, 3, "ThirdRequest");
    }

Conclusion

In this example we have learned how to create and use a custom injection resolver and a request scoped context. We have done so with a fake HttpServer example, that takes requests from a fake network and passes values to services based on fields in the HttpRequest. We have seen how the custom resolver can use data from annotations to further discover the values that should be given to the parameter. We have seen how the proxiable request context is used to ensure that the underlying request can change from request to request. We have shown how a custom resolver can customize the default Jakarta DI provider.

Back to the top