1 /**
2  * Copyright (C) 2008 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.inject;
18 
19 import static com.google.inject.matcher.Matchers.only;
20 
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.collect.Iterables;
24 import com.google.common.collect.Lists;
25 import com.google.inject.matcher.AbstractMatcher;
26 import com.google.inject.matcher.Matchers;
27 import com.google.inject.spi.ConstructorBinding;
28 
29 import junit.framework.TestCase;
30 
31 import org.aopalliance.intercept.MethodInterceptor;
32 import org.aopalliance.intercept.MethodInvocation;
33 
34 import java.lang.reflect.Method;
35 import java.util.Arrays;
36 import java.util.List;
37 import java.util.Queue;
38 import java.util.concurrent.atomic.AtomicInteger;
39 import java.util.concurrent.atomic.AtomicReference;
40 
41 /**
42  * @author jessewilson@google.com (Jesse Wilson)
43  */
44 public class MethodInterceptionTest extends TestCase {
45 
46   private AtomicInteger count = new AtomicInteger();
47 
48   private final class CountingInterceptor implements MethodInterceptor {
invoke(MethodInvocation methodInvocation)49     public Object invoke(MethodInvocation methodInvocation) throws Throwable {
50       count.incrementAndGet();
51       return methodInvocation.proceed();
52     }
53   }
54 
55   private final class ReturnNullInterceptor implements MethodInterceptor {
invoke(MethodInvocation methodInvocation)56     public Object invoke(MethodInvocation methodInvocation) throws Throwable {
57       return null;
58     }
59   }
60 
61   private final class NoOpInterceptor implements MethodInterceptor {
invoke(MethodInvocation methodInvocation)62     public Object invoke(MethodInvocation methodInvocation) throws Throwable {
63       return methodInvocation.proceed();
64     }
65   }
66 
testSharedProxyClasses()67   public void testSharedProxyClasses() {
68     Injector injector = Guice.createInjector(new AbstractModule() {
69       protected void configure() {
70         bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class)),
71             new ReturnNullInterceptor());
72       }
73     });
74 
75     Injector childOne = injector.createChildInjector(new AbstractModule() {
76       protected void configure() {
77         bind(Interceptable.class);
78       }
79     });
80 
81     Interceptable nullFoosOne = childOne.getInstance(Interceptable.class);
82     assertNotNull(nullFoosOne.bar());
83     assertNull(nullFoosOne.foo()); // confirm it's being intercepted
84 
85     Injector childTwo = injector.createChildInjector(new AbstractModule() {
86       protected void configure() {
87         bind(Interceptable.class);
88       }
89     });
90 
91     Interceptable nullFoosTwo = childTwo.getInstance(Interceptable.class);
92     assertNull(nullFoosTwo.foo()); // confirm it's being intercepted
93 
94     assertSame("Child injectors should share proxy classes, otherwise memory leaks!",
95         nullFoosOne.getClass(), nullFoosTwo.getClass());
96 
97     Injector injector2 = Guice.createInjector(new AbstractModule() {
98       protected void configure() {
99         bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class)),
100             new ReturnNullInterceptor());
101       }
102     });
103     Interceptable separateNullFoos = injector2.getInstance(Interceptable.class);
104     assertNull(separateNullFoos.foo()); // confirm it's being intercepted
105     assertSame("different injectors should share proxy classes, otherwise memory leaks!",
106         nullFoosOne.getClass(), separateNullFoos.getClass());
107   }
108 
testGetThis()109   public void testGetThis() {
110     final AtomicReference<Object> lastTarget = new AtomicReference<Object>();
111 
112     Injector injector = Guice.createInjector(new AbstractModule() {
113       protected void configure() {
114         bindInterceptor(Matchers.any(), Matchers.any(), new MethodInterceptor() {
115           public Object invoke(MethodInvocation methodInvocation) throws Throwable {
116             lastTarget.set(methodInvocation.getThis());
117             return methodInvocation.proceed();
118           }
119         });
120       }
121     });
122 
123     Interceptable interceptable = injector.getInstance(Interceptable.class);
124     interceptable.foo();
125     assertSame(interceptable, lastTarget.get());
126   }
127 
testInterceptingFinalClass()128   public void testInterceptingFinalClass() {
129     Injector injector = Guice.createInjector(new AbstractModule() {
130       protected void configure() {
131         bindInterceptor(Matchers.any(), Matchers.any(), new MethodInterceptor() {
132           public Object invoke(MethodInvocation methodInvocation) throws Throwable {
133             return methodInvocation.proceed();
134           }
135         });
136       }
137     });
138     try {
139       injector.getInstance(NotInterceptable.class);
140       fail();
141     } catch(ConfigurationException ce) {
142       assertEquals("Unable to method intercept: " + NotInterceptable.class.getName(),
143           Iterables.getOnlyElement(ce.getErrorMessages()).getMessage().toString());
144       assertEquals("Cannot subclass final class class " + NotInterceptable.class.getName(),
145           ce.getCause().getMessage());
146     }
147   }
148 
testSpiAccessToInterceptors()149   public void testSpiAccessToInterceptors() throws NoSuchMethodException {
150     final MethodInterceptor countingInterceptor = new CountingInterceptor();
151     final MethodInterceptor returnNullInterceptor = new ReturnNullInterceptor();
152     Injector injector = Guice.createInjector(new AbstractModule() {
153       protected void configure() {
154         bindInterceptor(Matchers.any(),Matchers.returns(only(Foo.class)),
155             countingInterceptor);
156         bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class).or(only(Bar.class))),
157             returnNullInterceptor);
158       }
159     });
160 
161     ConstructorBinding<?> interceptedBinding
162         = (ConstructorBinding<?>) injector.getBinding(Interceptable.class);
163     Method barMethod = Interceptable.class.getMethod("bar");
164     Method fooMethod = Interceptable.class.getMethod("foo");
165     assertEquals(ImmutableMap.<Method, List<MethodInterceptor>>of(
166         fooMethod, ImmutableList.of(countingInterceptor, returnNullInterceptor),
167         barMethod, ImmutableList.of(returnNullInterceptor)),
168         interceptedBinding.getMethodInterceptors());
169 
170     ConstructorBinding<?> nonInterceptedBinding
171         = (ConstructorBinding<?>) injector.getBinding(Foo.class);
172     assertEquals(ImmutableMap.<Method, List<MethodInterceptor>>of(),
173         nonInterceptedBinding.getMethodInterceptors());
174 
175     injector.getInstance(Interceptable.class).foo();
176     assertEquals("expected counting interceptor to be invoked first", 1, count.get());
177   }
178 
testInterceptedMethodThrows()179   public void testInterceptedMethodThrows() throws Exception {
180     Injector injector = Guice.createInjector(new AbstractModule() {
181       protected void configure() {
182         bindInterceptor(Matchers.any(), Matchers.any(), new CountingInterceptor());
183         bindInterceptor(Matchers.any(), Matchers.any(), new CountingInterceptor());
184       }
185     });
186 
187     Interceptable interceptable = injector.getInstance(Interceptable.class);
188     try {
189       interceptable.explode();
190       fail();
191     } catch (Exception e) {
192       // validate all causes.
193       for (Throwable t = e; t != null; t = t.getCause()) {
194         StackTraceElement[] stackTraceElement = t.getStackTrace();
195         assertEquals("explode", stackTraceElement[0].getMethodName());
196         assertEquals("invoke", stackTraceElement[1].getMethodName());
197         assertEquals("invoke", stackTraceElement[2].getMethodName());
198         assertEquals("testInterceptedMethodThrows", stackTraceElement[3].getMethodName());
199       }
200     }
201   }
202 
testNotInterceptedMethodsInInterceptedClassDontAddFrames()203   public void testNotInterceptedMethodsInInterceptedClassDontAddFrames() {
204     Injector injector = Guice.createInjector(new AbstractModule() {
205       protected void configure() {
206         bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class)),
207             new NoOpInterceptor());
208       }
209     });
210 
211     Interceptable interceptable = injector.getInstance(Interceptable.class);
212     assertNull(interceptable.lastElements);
213     interceptable.foo();
214     boolean cglibFound = false;
215     for (int i = 0; i < interceptable.lastElements.length; i++) {
216       if (interceptable.lastElements[i].toString().contains("cglib")) {
217         cglibFound = true;
218         break;
219       }
220     }
221     assertTrue(Arrays.toString(interceptable.lastElements), cglibFound);
222     cglibFound = false;
223 
224     interceptable.bar();
225     for (int i = 0; i < interceptable.lastElements.length; i++) {
226       if (interceptable.lastElements[i].toString().contains("cglib")) {
227         cglibFound = true;
228         break;
229       }
230     }
231     assertFalse(Arrays.toString(interceptable.lastElements), cglibFound);
232   }
233 
234   static class Foo {}
235   static class Bar {}
236 
237   public static class Interceptable {
238     StackTraceElement[] lastElements;
239 
foo()240     public Foo foo() {
241       lastElements = Thread.currentThread().getStackTrace();
242       return new Foo() {};
243     }
bar()244     public Bar bar() {
245       lastElements = Thread.currentThread().getStackTrace();
246       return new Bar() {};
247     }
explode()248     public String explode() throws Exception {
249       lastElements = Thread.currentThread().getStackTrace();
250       throw new Exception("kaboom!", new RuntimeException("boom!"));
251     }
252   }
253 
254   public static final class NotInterceptable {}
255 
testInterceptingNonBridgeWorks()256   public void testInterceptingNonBridgeWorks() {
257     Injector injector = Guice.createInjector(new AbstractModule() {
258       @Override
259       protected void configure() {
260         bind(Interface.class).to(Impl.class);
261         bindInterceptor(Matchers.any(), new AbstractMatcher<Method>() {
262           public boolean matches(Method t) {
263             return !t.isBridge() && t.getDeclaringClass() != Object.class;
264           }
265         }, new CountingInterceptor());
266       }
267     });
268     Interface intf = injector.getInstance(Interface.class);
269     assertEquals(0, count.get());
270     intf.aMethod(null);
271     assertEquals(1, count.get());
272   }
273 
274   static class ErasedType {}
275   static class RetType extends ErasedType {}
276   static abstract class Superclass<T extends ErasedType> {
aMethod(T t)277       public T aMethod(T t) { return null; }
278   }
279   public interface Interface {
aMethod(RetType obj)280       RetType aMethod(RetType obj);
281   }
282   public static class Impl extends Superclass<RetType> implements Interface {
283   }
284 
testInterceptionOrder()285   public void testInterceptionOrder() {
286     final List<String> callList = Lists.newArrayList();
287     Injector injector = Guice.createInjector(new AbstractModule() {
288       protected void configure() {
289         bindInterceptor(Matchers.any(), Matchers.any(),
290           new NamedInterceptor("a", callList),
291           new NamedInterceptor("b", callList),
292           new NamedInterceptor("c", callList));
293       }
294     });
295 
296     Interceptable interceptable = injector.getInstance(Interceptable.class);
297     assertEquals(0, callList.size());
298     interceptable.foo();
299     assertEquals(Arrays.asList("a", "b", "c"), callList);
300   }
301 
302   private final class NamedInterceptor implements MethodInterceptor {
303     private final String name;
304     final List<String> called;
305 
NamedInterceptor(String name, List<String> callList)306     NamedInterceptor(String name, List<String> callList) {
307       this.name = name;
308       this.called = callList;
309     }
310 
invoke(MethodInvocation methodInvocation)311     public Object invoke(MethodInvocation methodInvocation) throws Throwable {
312       called.add(name);
313       return methodInvocation.proceed();
314     }
315   }
316 
testDeDuplicateInterceptors()317   public void testDeDuplicateInterceptors() throws Exception {
318     Injector injector = Guice.createInjector(new AbstractModule() {
319       @Override protected void configure() {
320         CountingInterceptor interceptor = new CountingInterceptor();
321         bindInterceptor(Matchers.any(), Matchers.any(), interceptor);
322         bindInterceptor(Matchers.any(), Matchers.any(), interceptor);
323       }
324     });
325 
326     Interceptable interceptable = injector.getInstance(Interceptable.class);
327     interceptable.foo();
328     assertEquals(1, count.get());
329   }
330 
testCallLater()331   public void testCallLater() {
332     final Queue<Runnable> queue = Lists.newLinkedList();
333     Injector injector = Guice.createInjector(new AbstractModule() {
334       protected void configure() {
335         bindInterceptor(Matchers.any(), Matchers.any(), new CallLaterInterceptor(queue));
336       }
337     });
338 
339     Interceptable interceptable = injector.getInstance(Interceptable.class);
340     interceptable.foo();
341     assertNull(interceptable.lastElements);
342     assertEquals(1, queue.size());
343 
344     queue.remove().run();
345     assertNotNull(interceptable.lastElements);
346   }
347 
348   private final class CallLaterInterceptor implements MethodInterceptor {
349     private final Queue<Runnable> queue;
350 
CallLaterInterceptor(Queue<Runnable> queue)351     public CallLaterInterceptor(Queue<Runnable> queue) {
352       this.queue = queue;
353     }
354 
invoke(final MethodInvocation methodInvocation)355     public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
356       queue.add(new Runnable() {
357         @Override
358         public void run() {
359           try {
360             methodInvocation.proceed();
361           } catch (Throwable t) {
362             throw new RuntimeException(t);
363           }
364         }
365       });
366       return null;
367     }
368   }
369 }
370