Return-Path: X-Original-To: apmail-tomcat-dev-archive@www.apache.org Delivered-To: apmail-tomcat-dev-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 9A5651086E for ; Thu, 26 Sep 2013 22:55:53 +0000 (UTC) Received: (qmail 76622 invoked by uid 500); 26 Sep 2013 22:55:53 -0000 Delivered-To: apmail-tomcat-dev-archive@tomcat.apache.org Received: (qmail 76514 invoked by uid 500); 26 Sep 2013 22:55:52 -0000 Mailing-List: contact dev-help@tomcat.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: "Tomcat Developers List" Delivered-To: mailing list dev@tomcat.apache.org Received: (qmail 76505 invoked by uid 99); 26 Sep 2013 22:55:52 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 26 Sep 2013 22:55:52 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 26 Sep 2013 22:55:45 +0000 Received: from eris.apache.org (localhost [127.0.0.1]) by eris.apache.org (Postfix) with ESMTP id 1CF4E2388AB8 for ; Thu, 26 Sep 2013 22:55:23 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1526730 - in /tomcat/trunk: java/org/apache/catalina/loader/ java/org/apache/tomcat/ test/org/apache/catalina/loader/ webapps/docs/ Date: Thu, 26 Sep 2013 22:55:22 -0000 To: dev@tomcat.apache.org From: markt@apache.org X-Mailer: svnmailer-1.0.9 Message-Id: <20130926225523.1CF4E2388AB8@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Author: markt Date: Thu Sep 26 22:55:22 2013 New Revision: 1526730 URL: http://svn.apache.org/r1526730 Log: Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=55317 Facilitate weaving by allowing ClassFileTransformer to be added to WebppClassLoader Patch be Nick Williams Added: tomcat/trunk/java/org/apache/tomcat/InstrumentableClassLoader.java (with props) tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java (with props) tomcat/trunk/test/org/apache/catalina/loader/TesterNeverWeavedClass.java (with props) tomcat/trunk/test/org/apache/catalina/loader/TesterUnweavedClass.java (with props) Modified: tomcat/trunk/java/org/apache/catalina/loader/LocalStrings.properties tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java tomcat/trunk/webapps/docs/changelog.xml Modified: tomcat/trunk/java/org/apache/catalina/loader/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/loader/LocalStrings.properties?rev=1526730&r1=1526729&r2=1526730&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/loader/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/catalina/loader/LocalStrings.properties Thu Sep 26 22:55:22 2013 @@ -38,6 +38,11 @@ webappClassLoader.warnRequestThread=The webappClassLoader.warnThread=The web application [{0}] appears to have started a thread named [{1}] but has failed to stop it. This is very likely to create a memory leak. webappClassLoader.warnTimerThread=The web application [{0}] appears to have started a TimerThread named [{1}] via the java.util.Timer API but has failed to stop it. To prevent a memory leak, the timer (and hence the associated thread) has been forcibly canceled. webappClassLoader.wrongVersion=(unable to load class {0}) +webappClassLoader.addTransformer.illegalArgument=The web application [{0}] attempted to add a null class file transformer. +webappClassLoader.addTransformer.duplicate=Duplicate call to add class file transformer [{0}] to web application [{1}] ignored. +webappClassLoader.addTransformer=Added class file transformer [{0}] to web application [{1}]. +webappClassLoader.removeTransformer=Removed class file transformer [{0}] from web application [{1}]. +webappClassLoader.transformError=Instrumentation error: could not transform class [{0}] because its class file format is not legal. webappLoader.addRepository=Adding repository {0} webappLoader.deploy=Deploying class repositories to work directory {0} webappLoader.jarDeploy=Deploy JAR {0} to {1} Modified: tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java?rev=1526730&r1=1526729&r2=1526730&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java (original) +++ tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java Thu Sep 26 22:55:22 2013 @@ -22,6 +22,8 @@ import java.io.FileOutputStream; import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.lang.reflect.Field; @@ -51,6 +53,7 @@ import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ThreadPoolExecutor; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; @@ -66,6 +69,7 @@ import org.apache.catalina.LifecycleStat import org.apache.catalina.WebResource; import org.apache.catalina.WebResourceRoot; import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; +import org.apache.tomcat.InstrumentableClassLoader; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.IntrospectionUtils; import org.apache.tomcat.util.res.StringManager; @@ -105,15 +109,18 @@ import org.apache.tomcat.util.res.String * IMPLEMENTATION NOTE - No check for sealing violations or * security is made unless a security manager is present. *

+ * IMPLEMENTATION NOTE - As of 8.0, this class + * loader implements {@link InstrumentableClassLoader}, permitting web + * application classes to instrument other classes in the same web + * application. It does not permit instrumentation of system or container + * classes or classes in other web apps. * * @author Remy Maucherat * @author Craig R. McClanahan * @version $Id$ */ -public class WebappClassLoader - extends URLClassLoader - implements Lifecycle - { +public class WebappClassLoader extends URLClassLoader + implements Lifecycle, InstrumentableClassLoader { private static final org.apache.juli.logging.Log log= org.apache.juli.logging.LogFactory.getLog( WebappClassLoader.class ); @@ -126,6 +133,8 @@ public class WebappClassLoader private static final String JVN_THREAD_GROUP_SYSTEM = "system"; + private static final String CLASS_FILE_SUFFIX = ".class"; + static { JVM_THREAD_GROUP_NAMES.add(JVN_THREAD_GROUP_SYSTEM); JVM_THREAD_GROUP_NAMES.add("RMI Runtime"); @@ -462,6 +471,14 @@ public class WebappClassLoader */ private boolean clearReferencesHttpClientKeepAliveThread = true; + /** + * Holds the class file transformers decorating this class loader. The + * CopyOnWriteArrayList is thread safe. It is expensive on writes, but + * those should be rare. It is very fast on reads, since synchronization + * is not actually used. Importantly, the ClassLoader will never block + * iterating over the transformers while loading a class. + */ + private final List transformers = new CopyOnWriteArrayList<>(); // ------------------------------------------------------------- Properties @@ -736,6 +753,108 @@ public class WebappClassLoader // ------------------------------------------------------- Reloader Methods + /** + * Adds the specified class file transformer to this class loader. The + * transformer will then be able to modify the bytecode of any classes + * loaded by this class loader after the invocation of this method. + * + * @param transformer The transformer to add to the class loader + */ + @Override + public void addTransformer(ClassFileTransformer transformer) { + + if (transformer == null) { + throw new IllegalArgumentException(sm.getString( + "webappClassLoader.addTransformer.illegalArgument", getContextName())); + } + + if (this.transformers.contains(transformer)) { + // if the same instance of this transformer was already added, bail out + log.warn(sm.getString("webappClassLoader.addTransformer.duplicate", + transformer, getContextName())); + return; + } + this.transformers.add(transformer); + + log.info(sm.getString("webappClassLoader.addTransformer", transformer, getContextName())); + + } + + /** + * Removes the specified class file transformer from this class loader. + * It will no longer be able to modify the byte code of any classes + * loaded by the class loader after the invocation of this method. + * However, any classes already modified by this transformer will + * remain transformed. + * + * @param transformer The transformer to remove + */ + @Override + public void removeTransformer(ClassFileTransformer transformer) { + + if (transformer == null) { + return; + } + + if (this.transformers.remove(transformer)) { + log.info(sm.getString("webappClassLoader.removeTransformer", + transformer, getContextName())); + return; + } + + } + + /** + * Returns a copy of this class loader without any class file + * transformers. This is a tool often used by Java Persistence API + * providers to inspect entity classes in the absence of any + * instrumentation, something that can't be guaranteed within the + * context of a {@link ClassFileTransformer}'s + * {@link ClassFileTransformer#transform(ClassLoader, String, Class, + * ProtectionDomain, byte[]) transform} method. + *

+ * The returned class loader's resource cache will have been cleared + * so that classes already instrumented will not be retained or + * returned. + * + * @return the transformer-free copy of this class loader. + */ + @Override + public WebappClassLoader copyWithoutTransformers() { + + WebappClassLoader loader = new WebappClassLoader(this.parent); + + loader.antiJARLocking = this.antiJARLocking; + loader.resources = this.resources; + loader.delegate = this.delegate; + loader.lastJarAccessed = this.lastJarAccessed; + loader.repositoryPath = this.repositoryPath; + loader.repository = this.repository; + loader.jarPath = this.jarPath; + loader.loaderDir = this.loaderDir; + loader.canonicalLoaderDir = this.canonicalLoaderDir; + loader.started = this.started; + loader.needConvert = this.needConvert; + loader.clearReferencesStatic = this.clearReferencesStatic; + loader.clearReferencesStopThreads = this.clearReferencesStopThreads; + loader.clearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads; + loader.clearReferencesLogFactoryRelease = this.clearReferencesLogFactoryRelease; + loader.clearReferencesHttpClientKeepAliveThread = this.clearReferencesHttpClientKeepAliveThread; + + loader.repositoryURLs = this.repositoryURLs.clone(); + loader.jarFiles = this.jarFiles.clone(); + loader.jarRealFiles = this.jarRealFiles.clone(); + loader.jarNames = this.jarNames.clone(); + loader.lastModifiedDates = this.lastModifiedDates.clone(); + loader.paths = this.paths.clone(); + + loader.notFoundResources.putAll(this.notFoundResources); + loader.permissionList.addAll(this.permissionList); + loader.loaderPC.putAll(this.loaderPC); + + return loader; + + } /** * Set the place this ClassLoader can look for classes to be loaded. @@ -925,6 +1044,12 @@ public class WebappClassLoader sb.append(this.parent.toString()); sb.append("\r\n"); } + if (this.transformers.size() > 0) { + sb.append("----------> Class file transformers:\r\n"); + for (ClassFileTransformer transformer : this.transformers) { + sb.append(transformer).append("\r\n"); + } + } return (sb.toString()); } @@ -1180,7 +1305,7 @@ public class WebappClassLoader try { String repository = entry.codeBase.toString(); if ((repository.endsWith(".jar")) - && (!(name.endsWith(".class")))) { + && (!(name.endsWith(CLASS_FILE_SUFFIX)))) { // Copy binary content to the work directory if not present File resourceFile = new File(loaderDir, name); url = getURI(resourceFile); @@ -2549,7 +2674,7 @@ public class WebappClassLoader throw new ClassNotFoundException(name); String tempPath = name.replace('.', '/'); - String classPath = tempPath + ".class"; + String classPath = tempPath + CLASS_FILE_SUFFIX; ResourceEntry entry = null; @@ -2650,7 +2775,7 @@ public class WebappClassLoader * * @return the loaded resource, or null if the resource isn't found */ - protected ResourceEntry findResourceInternal(String name, String path) { + protected ResourceEntry findResourceInternal(final String name, final String path) { if (!started) { log.info(sm.getString("webappClassLoader.stopped", name)); @@ -2666,7 +2791,7 @@ public class WebappClassLoader int contentLength = -1; InputStream binaryStream = null; - boolean isClassResource = path.endsWith(".class"); + boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX); int jarFilesLength = jarFiles.length; @@ -2754,7 +2879,7 @@ public class WebappClassLoader } // Extract resources contained in JAR to the workdir - if (antiJARLocking && !(path.endsWith(".class"))) { + if (antiJARLocking && !(path.endsWith(CLASS_FILE_SUFFIX))) { byte[] buf = new byte[1024]; File resourceFile = new File (loaderDir, jarEntry.getName()); @@ -2765,7 +2890,7 @@ public class WebappClassLoader JarEntry jarEntry2 = entries.nextElement(); if (!(jarEntry2.isDirectory()) && (!jarEntry2.getName().endsWith - (".class"))) { + (CLASS_FILE_SUFFIX))) { resourceFile = new File (loaderDir, jarEntry2.getName()); try { @@ -2895,6 +3020,29 @@ public class WebappClassLoader } } + if (isClassResource && entry.binaryContent != null && + this.transformers.size() > 0) { + // If the resource is a class just being loaded, decorate it + // with any attached transformers + String className = name.endsWith(CLASS_FILE_SUFFIX) ? + name.substring(0, name.length() - CLASS_FILE_SUFFIX.length()) : name; + String internalName = className.replace(".", "/"); + + for (ClassFileTransformer transformer : this.transformers) { + try { + byte[] transformed = transformer.transform( + this, internalName, null, null, entry.binaryContent + ); + if (transformed != null) { + entry.binaryContent = transformed; + } + } catch (IllegalClassFormatException e) { + log.error(sm.getString("webappClassLoader.transformError", name), e); + return null; + } + } + } + // Add the entry in the local resource repository synchronized (resourceEntries) { // Ensures that all the threads which may be in a race to load @@ -3098,7 +3246,7 @@ public class WebappClassLoader } if (clazz == null) continue; - String name = triggers[i].replace('.', '/') + ".class"; + String name = triggers[i].replace('.', '/') + CLASS_FILE_SUFFIX; if (log.isDebugEnabled()) log.debug(" Checking for " + name); JarEntry jarEntry = jarFile.getJarEntry(name); Added: tomcat/trunk/java/org/apache/tomcat/InstrumentableClassLoader.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/InstrumentableClassLoader.java?rev=1526730&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/InstrumentableClassLoader.java (added) +++ tomcat/trunk/java/org/apache/tomcat/InstrumentableClassLoader.java Thu Sep 26 22:55:22 2013 @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat; + +import java.lang.instrument.ClassFileTransformer; + +/** + * Specifies a class loader capable of being decorated with + * {@link ClassFileTransformer}s. These transformers can instrument + * (or weave) the byte code of classes loaded through this class loader + * to alter their behavior. Currently only + * {@link org.apache.catalina.loader.WebappClassLoader} implements this + * interface. This allows web application frameworks or JPA providers + * bundled with a web application to instrument web application classes + * as necessary. + *

+ * You should always program against the methods of this interface + * (whether using reflection or otherwise). The methods in + * {@code WebappClassLoader} are protected by the default security + * manager if one is in use. + * + * @since 8.0, 7.0.44 + */ +public interface InstrumentableClassLoader { + + /** + * Adds the specified class file transformer to this class loader. The + * transformer will then be able to instrument the bytecode of any + * classes loaded by this class loader after the invocation of this + * method. + * + * @param classFileTransformer The transformer to add to the class loader + * @throws IllegalArgumentException if the {@literal transformer} is null. + */ + void addTransformer(ClassFileTransformer transformer); + + /** + * Removes the specified class file transformer from this class loader. + * It will no longer be able to instrument the byte code of any classes + * loaded by the class loader after the invocation of this method. + * However, any classes already instrumented by this transformer before + * this method call will remain in their instramented state. + * + * @param classFileTransformer The transformer to remove + */ + void removeTransformer(ClassFileTransformer transformer); + + /** + * Returns a copy of this class loader without any class file + * transformers. This is a tool often used by Java Persistence API + * providers to inspect entity classes in the absence of any + * instrumentation, something that can't be guaranteed within the + * context of a {@link ClassFileTransformer}'s + * {@link ClassFileTransformer#transform(ClassLoader, String, Class, + * java.security.ProtectionDomain, byte[]) transform} method. + *

+ * The returned class loader's resource cache will have been cleared + * so that classes already instrumented will not be retained or + * returned. + * + * @return the transformer-free copy of this class loader. + */ + ClassLoader copyWithoutTransformers(); + +} Propchange: tomcat/trunk/java/org/apache/tomcat/InstrumentableClassLoader.java ------------------------------------------------------------------------------ svn:eol-style = native Added: tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java?rev=1526730&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java (added) +++ tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java Thu Sep 26 22:55:22 2013 @@ -0,0 +1,429 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.catalina.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.http.fileupload.FileUtils; + +public class TestWebappClassLoaderWeaving extends TomcatBaseTest { + + private static final String PACKAGE_PREFIX = "org/apache/catalina/loader"; + + private static String WEBAPP_DOC_BASE; + + @BeforeClass + public static void setUpClass() throws Exception { + + WEBAPP_DOC_BASE = System.getProperty("java.io.tmpdir") + "/TestWebappClassLoaderWeaving"; + File classes = new File(WEBAPP_DOC_BASE + "/WEB-INF/classes/" + PACKAGE_PREFIX); + classes.mkdirs(); + + copyResource(PACKAGE_PREFIX + "/TesterNeverWeavedClass.class", + new File(classes, "TesterNeverWeavedClass.class")); + copyResource(PACKAGE_PREFIX + "/TesterUnweavedClass.class", + new File(classes, "TesterUnweavedClass.class")); + + } + + @AfterClass + public static void tearDownClass() throws Exception { + + FileUtils.deleteDirectory(new File(WEBAPP_DOC_BASE)); + + } + + private Tomcat tomcat; + private Context context; + private WebappClassLoader loader; + + @Before + @Override + public void setUp() throws Exception { + + super.setUp(); + + this.tomcat = getTomcatInstance(); + this.context = this.tomcat.addContext("/weaving", WEBAPP_DOC_BASE); + this.tomcat.start(); + + ClassLoader loader = this.context.getLoader().getClassLoader(); + assertNotNull("The class loader should not be null.", loader); + assertSame("The class loader is not correct.", WebappClassLoader.class, loader.getClass()); + + this.loader = (WebappClassLoader) loader; + + } + + @After + @Override + public void tearDown() throws Exception { + + try { + this.loader = null; + + this.context.stop(); + this.tomcat.getHost().removeChild(this.context); + this.context = null; + } finally { + super.tearDown(); + } + + } + + @Test + public void testNoWeaving() throws Exception { + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testAddingNullTransformerThrowsException() throws Exception { + + try { + this.loader.addTransformer(null); + fail("Expected exception IllegalArgumentException, got no exception."); + } catch (IllegalArgumentException ignore) { + // good + } + + // class loading should still work, no weaving + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testAddedTransformerInstrumentsClass1() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testAddedTransformerInstrumentsClass2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testTransformersExecuteInOrderAdded1() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testTransformersExecuteInOrderAdded2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments1() throws Exception { + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_1); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_2); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments3() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_1); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testCopiedClassLoaderExcludesResourcesAndTransformers() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + WebappClassLoader copiedLoader = this.loader.copyWithoutTransformers(); + + result = invokeDoMethodOnClass(copiedLoader, "TesterNeverWeavedClass"); + assertEquals("The third result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(copiedLoader, "TesterUnweavedClass"); + assertEquals("The fourth result is not correct.", "Hello, Unweaved World!", result); + + assertEquals("getAntiJARLocking did not match.", + Boolean.valueOf(this.loader.getAntiJARLocking()), + Boolean.valueOf(copiedLoader.getAntiJARLocking())); + assertEquals("getClearReferencesHttpClientKeepAliveThread did not match.", + Boolean.valueOf(this.loader.getClearReferencesHttpClientKeepAliveThread()), + Boolean.valueOf(copiedLoader.getClearReferencesHttpClientKeepAliveThread())); + assertEquals("getClearReferencesLogFactoryRelease did not match.", + Boolean.valueOf(this.loader.getClearReferencesLogFactoryRelease()), + Boolean.valueOf(copiedLoader.getClearReferencesLogFactoryRelease())); + assertEquals("getClearReferencesStatic did not match.", + Boolean.valueOf(this.loader.getClearReferencesStatic()), + Boolean.valueOf(copiedLoader.getClearReferencesStatic())); + assertEquals("getClearReferencesStopThreads did not match.", + Boolean.valueOf(this.loader.getClearReferencesStopThreads()), + Boolean.valueOf(copiedLoader.getClearReferencesStopThreads())); + assertEquals("getClearReferencesStopTimerThreads did not match.", + Boolean.valueOf(this.loader.getClearReferencesStopTimerThreads()), + Boolean.valueOf(copiedLoader.getClearReferencesStopTimerThreads())); + assertEquals("getContextName did not match.", this.loader.getContextName(), + copiedLoader.getContextName()); + assertEquals("getDelegate did not match.", + Boolean.valueOf(this.loader.getDelegate()), + Boolean.valueOf(copiedLoader.getDelegate())); + assertEquals("getJarPath did not match.", this.loader.getJarPath(), + copiedLoader.getJarPath()); + assertEquals("getURLs did not match.", this.loader.getURLs().length, + copiedLoader.getURLs().length); + assertSame("getParent did not match.", this.loader.getParent(), copiedLoader.getParent()); + + } + + private static void copyResource(String name, File file) throws Exception { + + InputStream is = TestWebappClassLoaderWeaving.class.getClassLoader() + .getResourceAsStream(name); + if (is == null) { + throw new IOException("Resource " + name + " not found on classpath."); + } + + FileOutputStream os = new FileOutputStream(file); + try { + for (int b = is.read(); b >= 0; b = is.read()) { + os.write(b); + } + } finally { + is.close(); + os.close(); + } + + } + + private static String invokeDoMethodOnClass(WebappClassLoader loader, String className) + throws Exception { + + Class c = loader.findClass("org.apache.catalina.loader." + className); + assertNotNull("The loaded class should not be null.", c); + + Method m = c.getMethod("doMethod"); + + Object o = c.newInstance(); + return (String) m.invoke(o); + + } + + private static class ReplacementTransformer implements ClassFileTransformer { + + private static final String CLASS_TO_WEAVE = PACKAGE_PREFIX + "/TesterUnweavedClass"; + + private final byte[] replacement; + + ReplacementTransformer(byte[] replacement) { + this.replacement = replacement; + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class x, + ProtectionDomain y, byte[] b) { + + if (CLASS_TO_WEAVE.equals(className)) { + return this.replacement; + } + + return null; + + } + + } + + /** + * Compiled version of org.apache.catalina.loader.TesterUnweavedClass, except that + * the doMethod method returns "Hello, Weaver #1!". Compiled with Oracle Java 1.6.0_51. + */ + private static final byte[] WEAVED_REPLACEMENT_1 = new byte[] { + -54, -2, -70, -66, 0, 0, 0, 50, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, + 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, + 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 100, + 111, 77, 101, 116, 104, 111, 100, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, + 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, + 105, 108, 101, 1, 0, 24, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, 101, + 100, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 17, 72, 101, + 108, 108, 111, 44, 32, 87, 101, 97, 118, 101, 114, 32, 35, 49, 33, 1, 0, 46, 111, 114, + 103, 47, 97, 112, 97, 99, 104, 101, 47, 99, 97, 116, 97, 108, 105, 110, 97, 47, 108, + 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, + 101, 100, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, + 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, + 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, + 0, 6, 0, 1, 0, 0, 0, 19, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, + 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 22, 0, 1, 0, 11, 0, 0, 0, + 2, 0, 12 + }; + + /** + * Compiled version of org.apache.catalina.loader.TesterUnweavedClass, except that + * the doMethod method returns "Hello, Weaver #2!". Compiled with Oracle Java 1.6.0_51. + */ + private static final byte[] WEAVED_REPLACEMENT_2 = new byte[] { + -54, -2, -70, -66, 0, 0, 0, 50, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, + 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, + 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 100, + 111, 77, 101, 116, 104, 111, 100, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, + 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, + 105, 108, 101, 1, 0, 24, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, 101, + 100, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 17, 72, 101, + 108, 108, 111, 44, 32, 87, 101, 97, 118, 101, 114, 32, 35, 50, 33, 1, 0, 46, 111, 114, + 103, 47, 97, 112, 97, 99, 104, 101, 47, 99, 97, 116, 97, 108, 105, 110, 97, 47, 108, + 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, + 101, 100, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, + 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, + 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, + 0, 6, 0, 1, 0, 0, 0, 19, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, + 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 22, 0, 1, 0, 11, 0, 0, 0, + 2, 0, 12 + }; + + /* + * The WEAVED_REPLACEMENT_1 and WEAVED_REPLACEMENT_2 field contents are generated using the + * following code. To regenerate them, alter the TesterUnweavedClass code as desired, recompile, + * and run this main method. + */ + public static void main(String... arguments) throws Exception { + InputStream input = TestWebappClassLoaderWeaving.class.getClassLoader() + .getResourceAsStream("org/apache/catalina/loader/TesterUnweavedClass.class"); + + StringBuilder builder = new StringBuilder(); + builder.append(" "); + + System.out.println(" private static final byte[] WEAVED_REPLACEMENT_1 = new byte[] {"); + try { + for (int i = 0, b = input.read(); b >= 0; i++, b = input.read()) { + String value = "" + ((byte)b); + if (builder.length() + value.length() > 97) { + builder.append(","); + System.out.println(builder.toString()); + builder = new StringBuilder(); + builder.append(" ").append(value); + } else { + if (i > 0) { + builder.append(", "); + } + builder.append(value); + } + } + System.out.println(builder.toString()); + } finally { + input.close(); + } + System.out.println(" }"); + } +} Propchange: tomcat/trunk/test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java ------------------------------------------------------------------------------ svn:eol-style = native Added: tomcat/trunk/test/org/apache/catalina/loader/TesterNeverWeavedClass.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/loader/TesterNeverWeavedClass.java?rev=1526730&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/catalina/loader/TesterNeverWeavedClass.java (added) +++ tomcat/trunk/test/org/apache/catalina/loader/TesterNeverWeavedClass.java Thu Sep 26 22:55:22 2013 @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.catalina.loader; + +public class TesterNeverWeavedClass { + + public String doMethod() { + return "This will never be weaved."; + } +} Propchange: tomcat/trunk/test/org/apache/catalina/loader/TesterNeverWeavedClass.java ------------------------------------------------------------------------------ svn:eol-style = native Added: tomcat/trunk/test/org/apache/catalina/loader/TesterUnweavedClass.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/loader/TesterUnweavedClass.java?rev=1526730&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/catalina/loader/TesterUnweavedClass.java (added) +++ tomcat/trunk/test/org/apache/catalina/loader/TesterUnweavedClass.java Thu Sep 26 22:55:22 2013 @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.catalina.loader; + +public class TesterUnweavedClass { + + public String doMethod() { + return "Hello, Unweaved World!"; + } +} Propchange: tomcat/trunk/test/org/apache/catalina/loader/TesterUnweavedClass.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1526730&r1=1526729&r2=1526730&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Thu Sep 26 22:55:22 2013 @@ -169,6 +169,10 @@ provided by Jasper. Includes removal of TldConfig lifecycle listener and associated Context properties. (jboynes) + + 55317: Facilitate weaving by allowing ClassFileTransformer to + be added to WebppClassLoader. Patch by Nick Williams. (markt) + --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org For additional commands, e-mail: dev-help@tomcat.apache.org