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.googlecode.guice;
18 
19 import static com.google.inject.matcher.Matchers.any;
20 
21 import com.google.inject.AbstractModule;
22 import com.google.inject.Binder;
23 import com.google.inject.Guice;
24 import com.google.inject.Injector;
25 import com.google.inject.Module;
26 import com.googlecode.guice.PackageVisibilityTestModule.PublicUserOfPackagePrivate;
27 
28 import junit.framework.TestCase;
29 
30 import org.aopalliance.intercept.MethodInterceptor;
31 import org.aopalliance.intercept.MethodInvocation;
32 
33 import java.io.File;
34 import java.lang.ref.Reference;
35 import java.lang.ref.WeakReference;
36 import java.lang.reflect.Constructor;
37 import java.lang.reflect.Method;
38 import java.net.MalformedURLException;
39 import java.net.URL;
40 import java.net.URLClassLoader;
41 import java.util.concurrent.TimeoutException;
42 
43 /**
44  * This test is in a separate package so we can test package-level visibility
45  * with confidence.
46  *
47  * @author mcculls@gmail.com (Stuart McCulloch)
48  */
49 public class BytecodeGenTest extends TestCase {
50 
51   private final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
52 
53   private final Module interceptorModule = new AbstractModule() {
54     protected void configure() {
55       bindInterceptor(any(), any(), new MethodInterceptor() {
56         public Object invoke(MethodInvocation chain)
57             throws Throwable {
58           return chain.proceed() + " WORLD";
59         }
60       });
61     }
62   };
63 
64   private final Module noopInterceptorModule = new AbstractModule() {
65       protected void configure() {
66         bindInterceptor(any(), any(), new MethodInterceptor() {
67           public Object invoke(MethodInvocation chain)
68               throws Throwable {
69             return chain.proceed();
70           }
71         });
72       }
73     };
74 
testPackageVisibility()75   public void testPackageVisibility() {
76     Injector injector = Guice.createInjector(new PackageVisibilityTestModule());
77     injector.getInstance(PublicUserOfPackagePrivate.class); // This must pass.
78   }
79 
testInterceptedPackageVisibility()80   public void testInterceptedPackageVisibility() {
81     Injector injector = Guice.createInjector(interceptorModule, new PackageVisibilityTestModule());
82     injector.getInstance(PublicUserOfPackagePrivate.class); // This must pass.
83   }
84 
testEnhancerNaming()85   public void testEnhancerNaming() {
86     Injector injector = Guice.createInjector(interceptorModule, new PackageVisibilityTestModule());
87     PublicUserOfPackagePrivate pupp = injector.getInstance(PublicUserOfPackagePrivate.class);
88     assertTrue(pupp.getClass().getName().startsWith(
89         PublicUserOfPackagePrivate.class.getName() + "$$EnhancerByGuice$$"));
90   }
91 
92   // TODO(sameb): Figure out how to test FastClass naming tests.
93 
94   /**
95    * Custom URL classloader with basic visibility rules
96    */
97   static class TestVisibilityClassLoader
98       extends URLClassLoader {
99 
100     boolean hideInternals;
101 
TestVisibilityClassLoader(boolean hideInternals)102     public TestVisibilityClassLoader(boolean hideInternals) {
103       super(new URL[0]);
104 
105       this.hideInternals = hideInternals;
106 
107       final String[] classpath = System.getProperty("java.class.path").split(File.pathSeparator);
108       for (final String element : classpath) {
109         try {
110           // is it a remote/local URL?
111           addURL(new URL(element));
112         } catch (final MalformedURLException e1) {
113           try {
114             // nope - perhaps it's a filename?
115             addURL(new File(element).toURI().toURL());
116           } catch (final MalformedURLException e2) {
117             throw new RuntimeException(e1);
118           }
119         }
120       }
121     }
122 
123     /**
124      * Classic parent-delegating classloaders are meant to override findClass.
125      * However, non-delegating classloaders (as used in OSGi) instead override
126      * loadClass to provide support for "class-space" separation.
127      */
128     @Override
loadClass(final String name, final boolean resolve)129     protected Class<?> loadClass(final String name, final boolean resolve)
130         throws ClassNotFoundException {
131 
132       synchronized (this) {
133         // check our local cache to avoid duplicates
134         final Class<?> clazz = findLoadedClass(name);
135         if (clazz != null) {
136           return clazz;
137         }
138       }
139 
140       if (name.startsWith("java.")) {
141 
142         // standard bootdelegation of java.*
143         return super.loadClass(name, resolve);
144 
145       } else if (!name.contains(".internal.") && !name.contains(".cglib.")) {
146 
147         /*
148          * load public and test classes directly from the classpath - we don't
149          * delegate to our parent because then the loaded classes would also be
150          * able to see private internal Guice classes, as they are also loaded
151          * by the parent classloader.
152          */
153         final Class<?> clazz = findClass(name);
154         if (resolve) {
155           resolveClass(clazz);
156         }
157         return clazz;
158       }
159 
160       // hide internal non-test classes
161       if (hideInternals) {
162         throw new ClassNotFoundException();
163       }
164       return super.loadClass(name, resolve);
165     }
166   }
167 
168   /** as loaded by another class loader */
169   private Class<ProxyTest> proxyTestClass;
170   private Class<ProxyTestImpl> realClass;
171   private Module testModule;
172 
173   @SuppressWarnings("unchecked")
setUp()174   protected void setUp() throws Exception {
175     super.setUp();
176 
177     ClassLoader testClassLoader = new TestVisibilityClassLoader(true);
178     proxyTestClass = (Class<ProxyTest>) testClassLoader.loadClass(ProxyTest.class.getName());
179     realClass = (Class<ProxyTestImpl>) testClassLoader.loadClass(ProxyTestImpl.class.getName());
180 
181     testModule = new AbstractModule() {
182       public void configure() {
183         bind(proxyTestClass).to(realClass);
184       }
185     };
186   }
187 
188   interface ProxyTest {
sayHello()189     String sayHello();
190   }
191 
192   /**
193    * Note: this class must be marked as public or protected so that the Guice
194    * custom classloader will intercept it. Private and implementation classes
195    * are not intercepted by the custom classloader.
196    *
197    * @see com.google.inject.internal.BytecodeGen.Visibility
198    */
199   public static class ProxyTestImpl implements ProxyTest {
200 
201     static {
202       //System.out.println(ProxyTestImpl.class.getClassLoader());
203     }
204 
sayHello()205     public String sayHello() {
206       return "HELLO";
207     }
208   }
209 
testProxyClassLoading()210   public void testProxyClassLoading() throws Exception {
211     Object testObject = Guice.createInjector(interceptorModule, testModule)
212         .getInstance(proxyTestClass);
213 
214     // verify method interception still works
215     Method m = realClass.getMethod("sayHello");
216     assertEquals("HELLO WORLD", m.invoke(testObject));
217   }
218 
testSystemClassLoaderIsUsedIfProxiedClassUsesIt()219   public void testSystemClassLoaderIsUsedIfProxiedClassUsesIt() {
220     ProxyTest testProxy = Guice.createInjector(interceptorModule, new Module() {
221       public void configure(Binder binder) {
222         binder.bind(ProxyTest.class).to(ProxyTestImpl.class);
223       }
224     }).getInstance(ProxyTest.class);
225 
226     if (ProxyTest.class.getClassLoader() == systemClassLoader) {
227       assertSame(testProxy.getClass().getClassLoader(), systemClassLoader);
228     } else {
229       assertNotSame(testProxy.getClass().getClassLoader(), systemClassLoader);
230     }
231   }
232 
testProxyClassUnloading()233   public void testProxyClassUnloading() {
234     Object testObject = Guice.createInjector(interceptorModule, testModule)
235         .getInstance(proxyTestClass);
236     assertNotNull(testObject.getClass().getClassLoader());
237     assertNotSame(testObject.getClass().getClassLoader(), systemClassLoader);
238 
239     // take a weak reference to the generated proxy class
240     Reference<Class<?>> clazzRef = new WeakReference<Class<?>>(testObject.getClass());
241 
242     assertNotNull(clazzRef.get());
243 
244     // null the proxy
245     testObject = null;
246 
247     /*
248      * this should be enough to queue the weak reference
249      * unless something is holding onto it accidentally.
250      */
251     final int MAX_COUNT = 100;
252     String[] buf;
253     System.gc();
254     //TODO(cgruber): Use com.google.common.testing.GcFinalization and a countdown latch to un-flake.
255     for (int count = 0 ; clazzRef.get() != null ; count++) {
256       buf = new String[8 * 1024 * 1024];
257       buf = null;
258       System.gc();
259       assertTrue("Timeout waiting for class to be unloaded.  This may be a flaky result.",
260           count <= MAX_COUNT);
261     }
262 
263     // This test could be somewhat flaky when the GC isn't working.
264     // If it fails, run the test again to make sure it's failing reliably.
265     assertNull("Proxy class was not unloaded.", clazzRef.get());
266   }
267 
testProxyingPackagePrivateMethods()268   public void testProxyingPackagePrivateMethods() {
269     Injector injector = Guice.createInjector(interceptorModule);
270     assertEquals("HI WORLD", injector.getInstance(PackageClassPackageMethod.class).sayHi());
271     assertEquals("HI WORLD", injector.getInstance(PublicClassPackageMethod.class).sayHi());
272     assertEquals("HI WORLD", injector.getInstance(ProtectedClassProtectedMethod.class).sayHi());
273   }
274 
275   static class PackageClassPackageMethod {
sayHi()276     String sayHi() {
277       return "HI";
278     }
279   }
280 
281   public static class PublicClassPackageMethod {
sayHi()282     String sayHi() {
283       return "HI";
284     }
285   }
286 
287   protected static class ProtectedClassProtectedMethod {
sayHi()288     protected String sayHi() {
289       return "HI";
290     }
291   }
292 
293   static class Hidden {
294   }
295 
296   public static class HiddenMethodReturn {
method()297     public Hidden method() {
298       return new Hidden();
299     }
300   }
301 
302   public static class HiddenMethodParameter {
method(Hidden h)303     public void method(Hidden h) {
304     }
305   }
306 
testClassLoaderBridging()307   public void testClassLoaderBridging() throws Exception {
308     ClassLoader testClassLoader = new TestVisibilityClassLoader(false);
309 
310     Class hiddenMethodReturnClass = testClassLoader.loadClass(HiddenMethodReturn.class.getName());
311     Class hiddenMethodParameterClass = testClassLoader.loadClass(HiddenMethodParameter.class.getName());
312 
313     Injector injector = Guice.createInjector(noopInterceptorModule);
314 
315     Class hiddenClass = testClassLoader.loadClass(Hidden.class.getName());
316     Constructor ctor = hiddenClass.getDeclaredConstructor();
317 
318     ctor.setAccessible(true);
319 
320     // don't use bridging for proxies with private parameters
321     Object o1 = injector.getInstance(hiddenMethodParameterClass);
322     o1.getClass().getDeclaredMethod("method", hiddenClass).invoke(o1, ctor.newInstance());
323 
324     // don't use bridging for proxies with private return types
325     Object o2 = injector.getInstance(hiddenMethodReturnClass);
326     o2.getClass().getDeclaredMethod("method").invoke(o2);
327   }
328 }
329