package org.hotswap.agent.util.classloader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hotswap.agent.javassist.CannotCompileException;
import org.hotswap.agent.javassist.ClassPool;
import org.hotswap.agent.javassist.CtClass;
import org.hotswap.agent.javassist.LoaderClassPath;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.util.scanner.ClassPathScanner;
import org.hotswap.agent.util.scanner.Scanner;
import org.hotswap.agent.util.scanner.ScannerVisitor;
/**
* Classloader patch which will redefine each patch via Javassist in the target classloader.
* <p/>
* Note that the class will typically be already accessible by parent classloader, but if it
* is loaded from parent classloader, it does not have access to other child classloader classes.
* <p/>
* Redefine will work only if the class was not loaded by the child classloader. This may not be used
* for Plugin class itself, because some target library classes may be enhanced by plugin reference
* (e.g. to set some initialized property). Although the class resides in parent classloader it cannot
* be redefined in child classloader with other definition - the classloader already knows about this class.
* This is the reason, why plugin class cannot be executed in child classloader.
*
* @author Jiri Bubnik
*/
public class ClassLoaderDefineClassPatcher implements ClassLoaderPatcher {
private static AgentLogger LOGGER = AgentLogger.getLogger(ClassLoaderDefineClassPatcher.class);
private static Map<String, List<byte[]>> pluginClassCache = new HashMap<String, List<byte[]>>();
@Override
public void patch(final ClassLoader classLoaderFrom, final String pluginPath,
final ClassLoader classLoaderTo, final ProtectionDomain protectionDomain) {
List<byte[]> cache = getPluginCache(classLoaderFrom, pluginPath);
if (cache != null) {
final ClassPool cp = new ClassPool();
cp.appendClassPath(new LoaderClassPath(getClass().getClassLoader()));
for (byte[] pluginBytes: cache) {
CtClass pluginClass = null;
try {
// force to load class in classLoaderFrom (it may not yet be loaded) and if the classLoaderTo
// is parent of classLoaderFrom, after definition in classLoaderTo will classLoaderFrom return
// class from parent classloader instead own definition (hence change of behaviour).
InputStream is = new ByteArrayInputStream(pluginBytes);
pluginClass = cp.makeClass(is);
try {
classLoaderFrom.loadClass(pluginClass.getName());
} catch (NoClassDefFoundError e) {
LOGGER.trace("Skipping class loading {} in classloader {} - " +
"class has probably unresolvable dependency.", pluginClass.getName(), classLoaderTo);
}
// and load the class in classLoaderTo as well. NOw the class is defined in BOTH classloaders.
pluginClass.toClass(classLoaderTo, protectionDomain);
} catch (CannotCompileException e) {
LOGGER.trace("Skipping class definition {} in app classloader {} - " +
"class is probably already defined.", pluginClass.getName(), classLoaderTo);
} catch (NoClassDefFoundError e) {
LOGGER.trace("Skipping class definition {} in app classloader {} - " +
"class has probably unresolvable dependency.", pluginClass.getName(), classLoaderTo);
} catch (Throwable e) {
LOGGER.trace("Skipping class definition app classloader {} - " +
"unknown error.", e, classLoaderTo);
}
}
}
LOGGER.debug("Classloader {} patched with plugin classes from agent classloader {}.", classLoaderTo, classLoaderFrom);
}
private List<byte[]> getPluginCache(final ClassLoader classLoaderFrom, final String pluginPath) {
List<byte[]> ret = null;
synchronized(pluginClassCache) {
ret = pluginClassCache.get(pluginPath);
if (ret == null) {
final List<byte[]> retList = new ArrayList<byte[]>();
Scanner scanner = new ClassPathScanner();
try {
scanner.scan(classLoaderFrom, pluginPath, new ScannerVisitor() {
@Override
public void visit(InputStream file) throws IOException {
// skip plugin classes
// TODO this should be skipped only in patching application classloader. To copy
// classes into agent classloader, Plugin class must be copied as well
// if (patchClass.hasAnnotation(Plugin.class)) {
// LOGGER.trace("Skipping plugin class: " + patchClass.getName());
// return;
// }
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int readBytes;
byte[] data = new byte[16384];
while ((readBytes = file.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, readBytes);
}
buffer.flush();
retList.add(buffer.toByteArray());
}
});
} catch (IOException e) {
LOGGER.error("Exception while scanning 'org/hotswap/agent/plugin'", e);
}
ret = retList;
pluginClassCache.put(pluginPath, ret);
}
}
return ret;
}
@Override
public boolean isPatchAvailable(ClassLoader classLoader) {
// we can define class in any class loader
// exclude synthetic classloader where it does not make any sense
// sun.reflect.DelegatingClassLoader - created automatically by JVM to optimize reflection calls
return classLoader != null && !classLoader.getClass().getName().equals("sun.reflect.DelegatingClassLoader");
}
}