/* * Copyright (C) 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.googlecode.guice; import static com.google.inject.matcher.Matchers.any; import com.google.common.testing.GcFinalization; import com.google.inject.AbstractModule; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.googlecode.guice.BytecodeGenTest.LogCreator; import com.googlecode.guice.PackageVisibilityTestModule.PublicUserOfPackagePrivate; import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URLClassLoader; import javax.inject.Inject; import junit.framework.TestCase; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; /** * This test is in a separate package so we can test package-level visibility with confidence. * * @author mcculls@gmail.com (Stuart McCulloch) */ public class BytecodeGenTest extends TestCase { private final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); private final Module interceptorModule = new AbstractModule() { @Override protected void configure() { bindInterceptor( any(), any(), new MethodInterceptor() { @Override public Object invoke(MethodInvocation chain) throws Throwable { return chain.proceed() + " WORLD"; } }); } }; private final Module noopInterceptorModule = new AbstractModule() { @Override protected void configure() { bindInterceptor( any(), any(), new MethodInterceptor() { @Override public Object invoke(MethodInvocation chain) throws Throwable { return chain.proceed(); } }); } }; public void testPackageVisibility() { Injector injector = Guice.createInjector(new PackageVisibilityTestModule()); injector.getInstance(PublicUserOfPackagePrivate.class); // This must pass. } public void testInterceptedPackageVisibility() { Injector injector = Guice.createInjector(interceptorModule, new PackageVisibilityTestModule()); injector.getInstance(PublicUserOfPackagePrivate.class); // This must pass. } public void testEnhancerNaming() { Injector injector = Guice.createInjector(interceptorModule, new PackageVisibilityTestModule()); PublicUserOfPackagePrivate pupp = injector.getInstance(PublicUserOfPackagePrivate.class); assertTrue( pupp.getClass() .getName() .startsWith(PublicUserOfPackagePrivate.class.getName() + "$$EnhancerByGuice$$")); } // TODO(sameb): Figure out how to test FastClass naming tests. /** Custom URL classloader with basic visibility rules */ static class TestVisibilityClassLoader extends URLClassLoader { final boolean hideInternals; TestVisibilityClassLoader(boolean hideInternals) { this((URLClassLoader) TestVisibilityClassLoader.class.getClassLoader(), hideInternals); } TestVisibilityClassLoader(URLClassLoader classloader, boolean hideInternals) { super(classloader.getURLs(), classloader); this.hideInternals = hideInternals; } /** * Classic parent-delegating classloaders are meant to override findClass. However, * non-delegating classloaders (as used in OSGi) instead override loadClass to provide support * for "class-space" separation. */ @Override protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException { synchronized (this) { // check our local cache to avoid duplicates final Class<?> clazz = findLoadedClass(name); if (clazz != null) { return clazz; } } if (name.startsWith("java.")) { // standard bootdelegation of java.* return super.loadClass(name, resolve); } else if (!name.contains(".internal.") && !name.contains(".cglib.")) { /* * load public and test classes directly from the classpath - we don't * delegate to our parent because then the loaded classes would also be * able to see private internal Guice classes, as they are also loaded * by the parent classloader. */ final Class<?> clazz = findClass(name); if (resolve) { resolveClass(clazz); } return clazz; } // hide internal non-test classes if (hideInternals) { throw new ClassNotFoundException(); } return super.loadClass(name, resolve); } } /** as loaded by another class loader */ private Class<ProxyTest> proxyTestClass; private Class<ProxyTestImpl> realClass; private Module testModule; @Override @SuppressWarnings("unchecked") protected void setUp() throws Exception { super.setUp(); ClassLoader testClassLoader = new TestVisibilityClassLoader(true); proxyTestClass = (Class<ProxyTest>) testClassLoader.loadClass(ProxyTest.class.getName()); realClass = (Class<ProxyTestImpl>) testClassLoader.loadClass(ProxyTestImpl.class.getName()); testModule = new AbstractModule() { @Override public void configure() { bind(proxyTestClass).to(realClass); } }; } interface ProxyTest { String sayHello(); } /** * Note: this class must be marked as public or protected so that the Guice custom classloader * will intercept it. Private and implementation classes are not intercepted by the custom * classloader. * * @see com.google.inject.internal.BytecodeGen.Visibility */ public static class ProxyTestImpl implements ProxyTest { static { //System.out.println(ProxyTestImpl.class.getClassLoader()); } @Override public String sayHello() { return "HELLO"; } } public void testProxyClassLoading() throws Exception { Object testObject = Guice.createInjector(interceptorModule, testModule).getInstance(proxyTestClass); // verify method interception still works Method m = realClass.getMethod("sayHello"); assertEquals("HELLO WORLD", m.invoke(testObject)); } public void testSystemClassLoaderIsUsedIfProxiedClassUsesIt() { ProxyTest testProxy = Guice.createInjector( interceptorModule, new Module() { @Override public void configure(Binder binder) { binder.bind(ProxyTest.class).to(ProxyTestImpl.class); } }) .getInstance(ProxyTest.class); if (ProxyTest.class.getClassLoader() == systemClassLoader) { assertSame(testProxy.getClass().getClassLoader(), systemClassLoader); } else { assertNotSame(testProxy.getClass().getClassLoader(), systemClassLoader); } } public void testProxyClassUnloading() { Object testObject = Guice.createInjector(interceptorModule, testModule).getInstance(proxyTestClass); assertNotNull(testObject.getClass().getClassLoader()); assertNotSame(testObject.getClass().getClassLoader(), systemClassLoader); // take a weak reference to the generated proxy class WeakReference<Class<?>> clazzRef = new WeakReference<Class<?>>(testObject.getClass()); assertNotNull(clazzRef.get()); // null the proxy testObject = null; /* * this should be enough to queue the weak reference * unless something is holding onto it accidentally. */ GcFinalization.awaitClear(clazzRef); // This test could be somewhat flaky when the GC isn't working. // If it fails, run the test again to make sure it's failing reliably. assertNull("Proxy class was not unloaded.", clazzRef.get()); } public void testProxyingPackagePrivateMethods() { Injector injector = Guice.createInjector(interceptorModule); assertEquals("HI WORLD", injector.getInstance(PackageClassPackageMethod.class).sayHi()); assertEquals("HI WORLD", injector.getInstance(PublicClassPackageMethod.class).sayHi()); assertEquals("HI WORLD", injector.getInstance(ProtectedClassProtectedMethod.class).sayHi()); } static class PackageClassPackageMethod { String sayHi() { return "HI"; } } public static class PublicClassPackageMethod { String sayHi() { return "HI"; } } protected static class ProtectedClassProtectedMethod { protected String sayHi() { return "HI"; } } static class Hidden {} public static class HiddenMethodReturn { public Hidden method() { return new Hidden(); } } public static class HiddenMethodParameter { public void method(Hidden h) {} } public void testClassLoaderBridging() throws Exception { ClassLoader testClassLoader = new TestVisibilityClassLoader(false); Class hiddenMethodReturnClass = testClassLoader.loadClass(HiddenMethodReturn.class.getName()); Class hiddenMethodParameterClass = testClassLoader.loadClass(HiddenMethodParameter.class.getName()); Injector injector = Guice.createInjector(noopInterceptorModule); Class hiddenClass = testClassLoader.loadClass(Hidden.class.getName()); Constructor ctor = hiddenClass.getDeclaredConstructor(); ctor.setAccessible(true); // don't use bridging for proxies with private parameters Object o1 = injector.getInstance(hiddenMethodParameterClass); o1.getClass().getDeclaredMethod("method", hiddenClass).invoke(o1, ctor.newInstance()); // don't use bridging for proxies with private return types Object o2 = injector.getInstance(hiddenMethodReturnClass); o2.getClass().getDeclaredMethod("method").invoke(o2); } // This tests for a situation where a osgi bundle contains a version of guice. When guice // generates a fast class it will use a bridge classloader public void testFastClassUsesBridgeClassloader() throws Throwable { Injector injector = Guice.createInjector(); // These classes are all in the same classloader as guice itself, so other than the private one // they can all be fast class invoked injector.getInstance(PublicInject.class).assertIsFastClassInvoked(); injector.getInstance(ProtectedInject.class).assertIsFastClassInvoked(); injector.getInstance(PackagePrivateInject.class).assertIsFastClassInvoked(); injector.getInstance(PrivateInject.class).assertIsReflectionInvoked(); // This classloader will load the types in an loader with a different version of guice/cglib // this prevents the use of fastclass for all but the public types (where the bridge // classloader can be used). MultipleVersionsOfGuiceClassLoader fakeLoader = new MultipleVersionsOfGuiceClassLoader(); injector .getInstance(fakeLoader.loadLogCreatorType(PublicInject.class)) .assertIsFastClassInvoked(); injector .getInstance(fakeLoader.loadLogCreatorType(ProtectedInject.class)) .assertIsReflectionInvoked(); injector .getInstance(fakeLoader.loadLogCreatorType(PackagePrivateInject.class)) .assertIsReflectionInvoked(); injector .getInstance(fakeLoader.loadLogCreatorType(PrivateInject.class)) .assertIsReflectionInvoked(); } // This classloader simulates an OSGI environment where a bundle has a conflicting definition of // cglib (or guice). This is sort of the opposite of the BridgeClassloader and is meant to test // its use. static class MultipleVersionsOfGuiceClassLoader extends URLClassLoader { MultipleVersionsOfGuiceClassLoader() { this((URLClassLoader) MultipleVersionsOfGuiceClassLoader.class.getClassLoader()); } MultipleVersionsOfGuiceClassLoader(URLClassLoader classloader) { super(classloader.getURLs(), classloader); } public Class<? extends LogCreator> loadLogCreatorType(Class<? extends LogCreator> cls) throws ClassNotFoundException { return loadClass(cls.getName()).asSubclass(LogCreator.class); } /** * Classic parent-delegating classloaders are meant to override findClass. However, * non-delegating classloaders (as used in OSGi) instead override loadClass to provide support * for "class-space" separation. */ @Override protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException { synchronized (this) { // check our local cache to avoid duplicates final Class<?> clazz = findLoadedClass(name); if (clazz != null) { return clazz; } } if (name.startsWith("java.") || name.startsWith("javax.") || name.equals(LogCreator.class.getName()) || (!name.startsWith("com.google.inject.") && !name.contains(".cglib.") && !name.startsWith("com.googlecode.guice"))) { // standard parent delegation return super.loadClass(name, resolve); } else { // load a new copy of the class final Class<?> clazz = findClass(name); if (resolve) { resolveClass(clazz); } return clazz; } } } public static class LogCreator { final Throwable caller; public LogCreator() { this.caller = new Throwable(); } void assertIsFastClassInvoked() throws Throwable { // 2 because the first 2 elements are // LogCreator.<init>() // Subclass.<init>() if (!caller.getStackTrace()[2].getClassName().contains("$$FastClassByGuice$$")) { throw new AssertionError("Caller was not FastClass").initCause(caller); } } void assertIsReflectionInvoked() throws Throwable { // Scan for a call to Constructor.newInstance, but stop if we see the test itself. for (StackTraceElement element : caller.getStackTrace()) { if (element.getClassName().equals(BytecodeGenTest.class.getName())) { // break when we hit the test method. break; } if (element.getClassName().equals(Constructor.class.getName()) && element.getMethodName().equals("newInstance")) { return; } } throw new AssertionError("Caller was not Constructor.newInstance").initCause(caller); } } public static class PublicInject extends LogCreator { @Inject public PublicInject() {} } static class PackagePrivateInject extends LogCreator { @Inject PackagePrivateInject() {} } protected static class ProtectedInject extends LogCreator { @Inject protected ProtectedInject() {} } private static class PrivateInject extends LogCreator { @Inject private PrivateInject() {} } }