Search code examples
javadependency-injectionaopguice

Unable to get the hang of Guice Method Interceptor (Null Pointer Exception during bindInterceptor)


I have an interceptor to throttle requests to arbitrary APIs. I am trying to write an annotation that supports plugging in a TPS value so that any method can be rate limited.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimitMethodAnnotation {

    // Permissible transactions per second.
    long tps() default Long.MAX_VALUE;

    // The identifier for the rate limiter. A distinct token bucket is defined
    // per id.
    String id();
}

While the implementation of the Interceptor is as follows:-

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.isomorphism.util.TokenBucket;
import org.isomorphism.util.TokenBuckets;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * Implementation of the rate limiter.
 */
public class RateLimitMethodAnnotationInterceptor implements    MethodInterceptor {

    private static final ConcurrentHashMap<String, TokenBucket>
            TOKEN_BUCKET_MAP = new ConcurrentHashMap<String,    TokenBucket>();

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        final RateLimitMethodAnnotation rateLimitMethod =
             methodInvocation.getMethod().getAnnotation(RateLimitMethodAnnotation.class);

        final String rateLimitId = rateLimitMethod.id();
        final long tps = rateLimitMethod.tps();

        boolean proceedMethodCall = tryProceed(rateLimitId, tps);

        while(!proceedMethodCall) {
            Thread.sleep(getDurationTillRefillInMilliSecond(rateLimitId, tps));
            proceedMethodCall = tryProceed(rateLimitId, tps);
        }

        return methodInvocation.proceed();
    }

    private boolean tryProceed(final String tokenBucketId, final long tps) {

        final TokenBucket tokenBucket = TOKEN_BUCKET_MAP.get(tokenBucketId);

        if (tokenBucket == null) {
            TOKEN_BUCKET_MAP.put(tokenBucketId, buildTokenBucket(tps));
        }

        return tokenBucket.tryConsume();
    }

    private long getDurationTillRefillInMilliSecond(final String tokenBucketId, long tps) {
        final TokenBucket tokenBucket = TOKEN_BUCKET_MAP.get(tokenBucketId);

        if (tokenBucket == null) {
            TOKEN_BUCKET_MAP.put(tokenBucketId, buildTokenBucket(tps));
        }

        return   tokenBucket.getDurationUntilNextRefill(TimeUnit.MILLISECONDS);

    }

    private TokenBucket buildTokenBucket(final long tps) {
        return TokenBuckets.builder().withCapacity(tps)
               .withFixedIntervalRefillStrategy(1, 1, TimeUnit.SECONDS)
               .build();
    }
}

Now in order to define a binding I have used the following code :-

import com.google.inject.AbstractModule;
import com.google.inject.matcher.Matchers;

/**
 * Configuration for rate limiting.
 */
public class RateLimitConfig extends AbstractModule {

    public void configure() {
        bindInterceptor(Matchers.any(), 
            Matchers.annotatedWith(RateLimitMethodAnnotation.class),
            new RateLimitMethodAnnotationInterceptor());
    }
}

I wrote a very simple sanity test to prove the injection config works as follows:-

import org.junit.Test;

import static org.junit.Assert.assertTrue;

/**
 * Rate limit test class.
 */
public class TestRateLimit {

    final int TEST_VAL = 1;

    @Test
    public void testRateLimitInterceptorSanityTest() {
        final RateLimitConfig config = new RateLimitConfig();
        config.configure();

        int retVal = stubMethod();
        assertTrue(retVal == TEST_VAL);
    }

    @RateLimitMethodAnnotation(tps = Long.MAX_VALUE, id="stubMethod")
    public int stubMethod() {
        return TEST_VAL;
    }
}

I ended up with a NPE

 Running TestRateLimit
 Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed:   0.073 sec <<< FAILURE!
testRateLimitInterceptorSanityTest(TestRateLimit)  Time elapsed: 0.013 sec  <<< ERROR!
java.lang.NullPointerException
    at com.google.inject.AbstractModule.bindInterceptor(AbstractModule.java:167)
at org.isomorphism.annotation.RateLimitConfig.configure(RateLimitConfig.java:12)
     at org.isomorphism.annotation.TestRateLimit.testRateLimitInterceptorSanityTest(TestRateLimit.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)

I looked at the code here https://github.com/google/guice/blob/master/core/src/com/google/inject/AbstractModule.java but did not find anything useful. I debugged into the code but I cannot understand the data structures (and in order to fully understand the framework I have to devote hours, which I do not want to for simple tasks).

1. Even for a simple task like this, Guice should not throw an NPE even if 0 methods were annotated with the annotation I have in mind. 
2. Is the configure method never supposed to be called directly in code? If so there is no API given in AbstractModule nor documentation how to configure bindInterceptors. Taking the code out of RateLimitConfig did not work (after putting it into the test suite). 

Can anyone help me out with this?


Solution

  • You didn't create any injector in your test case:

    @Test
    public void testRateLimitInterceptorSanityTest() {
        final RateLimitConfig config = new RateLimitConfig();
        Injector injector = Guice.createInjector(config);
        TestRateLimit testInstance = injector.getInstance(TestRateLimit.class);
    
        int retVal = testInstance.stubMethod();
        assertTrue(retVal == TEST_VAL);
    }