Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use ServletScopes.scopeRequest() and ServletScopes.continueRequest()?

Tags:

java

guice

  1. How is one supposed to use ServletScopes.scopeRequest()?
  2. How do I get a reference to a @RequestScoped object inside the Callable?
  3. What's the point of seedMap? Is it meant to override the default binding?
  4. What's the difference between this method and ServletScopes.continueRequest()?
like image 373
Gili Avatar asked Feb 03 '23 11:02

Gili


1 Answers

Answering my own question:

  1. ServletScopes.scopeRequest() runs a Callable in a new request scope. Be careful not to reference objects across different scopes, otherwise you'll end up with threading issues such as trying to use a database connection that has already been closed by another request. static or top-level classes are your friend here.
  2. You inject the Callable before passing it into ServletScopes.scopeRequest(). For this reason, you must be careful what fields your Callable contains. More on this below.
  3. seedMap allows you to inject non-scoped objects into the scope. This is dangerous so be careful with what you inject.
  4. ServletScopes.continueRequest() is similar except that it runs inside an existing request scope. It takes a snapshot of the current HTTP scope and wraps it in a Callable. The original HTTP request completes (you return some response from the server) but then complete the actual operation asynchronously in a separate thread. When the Callable is invoked at some later time (in that separate thread) it will have access to the original HttpServletRequest but not the HTTP response or session.

So, what's the best way to do this?

If you don't need to pass user-objects into the Callable: Inject the Callable outside the request scope, and pass it into ServletScopes.scopeRequest(). The Callable may only reference Provider<Foo> instead of Foo, otherwise you'll end up with instances injected outside of the request scope.

If you need to pass user-objects into the Callable, read on.

Say you have a method that inserts names into a database. There are two ways for us to pass the name into the Callable.

Approach 1: Pass user-objects using a child module:

  1. Define InsertName, a Callable that inserts into the database:

    @RequestScoped
    private static class InsertName implements Callable<Boolean>
    {
      private final String name;
      private final Connection connection;
    
      @Inject
      public InsertName(@Named("name") String name, Connection connection)
      {
        this.name = name;
        this.connection = connection;
      }
    
      @Override
      public Boolean call()
      {
        try
        {
          boolean nameAlreadyExists = ...;
          if (!nameAlreadyExists)
          {
            // insert the name
            return true;
          }
          return false;
        }
        finally
        {
          connection.close();
        }
      }
    }
    
  2. Bind all user-objects in a child module and scope the callable using RequestInjector.scopeRequest():

    requestInjector.scopeRequest(InsertName.class, new AbstractModule()
    {
      @Override
      protected void configure()
      {
        bind(String.class).annotatedWith(Names.named("name")).toInstance("John");
      }
    })
    
  3. We instantiate a RequestInjector outside the request and it, in turn, injects a second Callable inside the request. The second Callable can reference Foo directly (no need for Providers) because it's injected inside the request scope.

import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.servlet.ServletScopes;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.Callable;

/**
 * Injects a Callable into a non-HTTP request scope.
 * <p/>
 * @author Gili Tzabari
 */
public final class RequestInjector
{
    private final Map<Key<?>, Object> seedMap = Collections.emptyMap();
    private final Injector injector;

    /**
     * Creates a new RequestInjector.
     */
    @Inject
    private RequestInjector(Injector injector)
    {
        this.injector = injector;
    }

    /**
     * Scopes a Callable in a non-HTTP request scope.
     * <p/>
     * @param <V> the type of object returned by the Callable
     * @param callable the class to inject and execute in the request scope
     * @param modules additional modules to install into the request scope
     * @return a wrapper that invokes delegate in the request scope
     */
    public <V> Callable<V> scopeRequest(final Class<? extends Callable<V>> callable,
        final Module... modules)
    {
        Preconditions.checkNotNull(callable, "callable may not be null");

        return ServletScopes.scopeRequest(new Callable<V>()
        {
            @Override
            public V call() throws Exception
            {
                return injector.createChildInjector(modules).getInstance(callable).call();
            }
        }, seedMap);
    }
}

Approach 2: Inject a Callable outside the request that references Provider<Foo>. The call() method can then get() the actual values inside the request scope. The object objects are passed in by way of a seedMap (I personally find this approach counter-intuitive):

  1. Define InsertName, a Callable that inserts into the database. Notice that unlike Approach 1, we must use Providers:

    @RequestScoped
    private static class InsertName implements Callable<Boolean>
    {
      private final Provider<String> name;
      private final Provider<Connection> connection;
    
      @Inject
      public InsertName(@Named("name") Provider<String> name, Provider<Connection> connection)
      {
        this.name = name;
        this.connection = connection;
      }
    
      @Override
      public Boolean call()
      {
        try
        {
          boolean nameAlreadyExists = ...;
          if (!nameAlreadyExists)
          {
            // insert the name
            return true;
          }
          return false;
        }
        finally
        {
          connection.close();
        }
      }
    }
    
  2. Create bogus bindings for the types you want to pass in. If you don't you will get: No implementation for String annotated with @com.google.inject.name.Named(value=name) was bound. https://stackoverflow.com/a/9014552/14731 explains why this is needed.

  3. Populate the seedMap with the desired values:

    ImmutableMap<Key<?>, Object> seedMap = ImmutableMap.<Key<?>, Object>of(Key.get(String.class, Names.named("name")), "john");
    
  4. Invoke ServletScopes.scopeRequest():

    ServletScopes.scopeRequest(injector.getInstance(InsertName.class), seedMap);
    
like image 164
Gili Avatar answered Feb 12 '23 09:02

Gili