package org.hotswap.agent.util;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import org.hotswap.agent.annotation.handler.PluginClassFileTransformer;
import org.hotswap.agent.command.Command;
import org.hotswap.agent.config.PluginManager;
import org.hotswap.agent.logging.AgentLogger;
/**
* Java instrumentation transformer.
* <p/>
* The is the single instance of transformer registered by HotswapAgent. It will delegate to plugins to
* do the transformer work.
*
* @author Jiri Bubnik
*/
public class HotswapTransformer implements ClassFileTransformer {
private static AgentLogger LOGGER = AgentLogger.getLogger(HotswapTransformer.class);
/**
* Exclude these classLoaders from initialization (system classloaders). Note that
*/
private static final Set<String> excludedClassLoaders = new HashSet<String>(Arrays.asList(
"sun.reflect.DelegatingClassLoader",
"org.apache.felix.framework.BundleWiringImpl$BundleClassLoader", // delegating ClassLoader in GlassFish
"org.apache.felix.framework.BundleWiringImpl$BundleClassLoaderJava5" // delegating ClassLoader in_GlassFish
));
private static class RegisteredTransformersRecord {
Pattern pattern;
List<ClassFileTransformer> transformerList = new LinkedList<ClassFileTransformer>();
}
protected Map<String, RegisteredTransformersRecord> registeredTransformers = new LinkedHashMap<String, RegisteredTransformersRecord>();
// keep track about which classloader requested which transformer
protected Map<ClassFileTransformer, ClassLoader> classLoaderTransformers = new LinkedHashMap<ClassFileTransformer, ClassLoader>();
protected Map<ClassLoader, Object> seenClassLoaders = new WeakHashMap<ClassLoader, Object>();
private List<Pattern> excludedClassLoaderPatterns;
/**
* @param excludedClassLoaderPatterns
* the excludedClassLoaderPatterns to set
*/
public void setExcludedClassLoaderPatterns(List<Pattern> excludedClassLoaderPatterns) {
this.excludedClassLoaderPatterns = excludedClassLoaderPatterns;
}
/**
* Register a transformer for a regexp matching class names.
* Used by {@link org.hotswap.agent.annotation.OnClassLoadEvent} annotation respective
* {@link org.hotswap.agent.annotation.handler.OnClassLoadedHandler}.
*
* @param classLoader the classloader to which this transformation is associated
* @param classNameRegexp regexp to match fully qualified class name.
* Because "." is any character in regexp, this will match / in the transform method as well
* (diffentence between java/lang/String and java.lang.String).
* @param transformer the transformer to be called for each class matching regexp.
*/
public void registerTransformer(ClassLoader classLoader, String classNameRegexp, ClassFileTransformer transformer) {
LOGGER.debug("Registering transformer for class regexp '{}'.", classNameRegexp);
String normalizeRegexp = normalizeTypeRegexp(classNameRegexp);
RegisteredTransformersRecord transformerRecord = registeredTransformers.get(normalizeRegexp);
if (transformerRecord == null) {
transformerRecord = new RegisteredTransformersRecord();
transformerRecord.pattern = Pattern.compile(normalizeRegexp);
registeredTransformers.put(normalizeRegexp, transformerRecord);
}
transformerRecord.transformerList.add(transformer);
// register classloader association to allow classloader unregistration
if (classLoader != null) {
classLoaderTransformers.put(transformer, classLoader);
}
}
/**
* Remove registered transformer.
*
* @param classNameRegexp regexp to match fully qualified class name.
* @param transformer currently registered transformer
*/
public void removeTransformer(String classNameRegexp, ClassFileTransformer transformer) {
String normalizeRegexp = normalizeTypeRegexp(classNameRegexp);
RegisteredTransformersRecord transformerRecord = registeredTransformers.get(normalizeRegexp);
if (transformerRecord != null) {
transformerRecord.transformerList.remove(transformer);
}
}
/**
* Remove all transformers registered with a classloader
* @param classLoader
*/
public void closeClassLoader(ClassLoader classLoader) {
for (Iterator<Map.Entry<ClassFileTransformer, ClassLoader>> entryIterator = classLoaderTransformers.entrySet().iterator();
entryIterator.hasNext(); ) {
Map.Entry<ClassFileTransformer, ClassLoader> entry = entryIterator.next();
if (entry.getValue().equals(classLoader)) {
entryIterator.remove();
for (RegisteredTransformersRecord transformerRecord : registeredTransformers.values())
transformerRecord.transformerList.remove(entry.getKey());
}
}
LOGGER.debug("All transformers removed for classLoader {}", classLoader);
}
/**
* Main transform method called by Java instrumentation.
* <p/>
* <p>It does not do the instrumentation itself, instead iterates registered transformers and compares
* registration class regexp - if the regexp matches, the classloader is called.
* <p/>
* <p>Note that class bytes may be send to multiple transformers, but the order is not defined.
*
* @see ClassFileTransformer#transform(ClassLoader, String, Class, java.security.ProtectionDomain, byte[])
*/
@Override
public byte[] transform(final ClassLoader classLoader, String className, Class<?> redefiningClass,
final ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
LOGGER.trace("Transform on class '{}' @{} redefiningClass '{}'.", className, classLoader, redefiningClass);
List<ClassFileTransformer> toApply = new LinkedList<>();
List<PluginClassFileTransformer> pluginTransformers = new LinkedList<>();
try {
// call transform on all registered transformers
boolean isRedefineEvent = (redefiningClass != null);
for (RegisteredTransformersRecord transformerRecord : new LinkedList<RegisteredTransformersRecord>(registeredTransformers.values())) {
if ((className != null && transformerRecord.pattern.matcher(className).matches()) ||
(redefiningClass != null && transformerRecord.pattern.matcher(redefiningClass.getName()).matches())) {
for (ClassFileTransformer transformer : new LinkedList<ClassFileTransformer>(transformerRecord.transformerList)) {
if(transformer instanceof PluginClassFileTransformer) {
PluginClassFileTransformer pcft = (PluginClassFileTransformer )transformer;
if (isRedefineEvent || pcft.acceptsDefineEvent()) {
if(!pcft.isPluginDisabled(classLoader)) {
pluginTransformers.add(pcft);
}
}
} else {
toApply.add(transformer);
}
}
}
}
} catch (Throwable t) {
LOGGER.error("Error transforming class '" + className + "'.", t);
}
if(!pluginTransformers.isEmpty()) {
pluginTransformers = reduce(classLoader, pluginTransformers, className);
}
if(toApply.isEmpty() && pluginTransformers.isEmpty()) {
LOGGER.trace("No transformers defing for {} ", className);
return bytes;
}
// ensure classloader initialized
ensureClassLoaderInitialized(classLoader, protectionDomain);
try {
byte[] result = bytes;
for(ClassFileTransformer transformer: pluginTransformers) {
LOGGER.trace("Transforming class '" + className + "' with transformer '" + transformer + "' " + "@ClassLoader" + classLoader + ".");
result = transformer.transform(classLoader, className, redefiningClass, protectionDomain, result);
}
for(ClassFileTransformer transformer: toApply) {
LOGGER.trace("Transforming class '" + className + "' with transformer '" + transformer + "' " + "@ClassLoader" + classLoader + ".");
result = transformer.transform(classLoader, className, redefiningClass, protectionDomain, result);
}
return result;
} catch (Throwable t) {
LOGGER.error("Error transforming class '" + className + "'.", t);
}
return bytes;
}
LinkedList<PluginClassFileTransformer> reduce(final ClassLoader classLoader, List<PluginClassFileTransformer> pluginCalls, String className) {
LinkedList<PluginClassFileTransformer> reduced = new LinkedList<>();
Map<String, PluginClassFileTransformer> fallbackMap = new HashMap<>();
for (PluginClassFileTransformer pcft : pluginCalls) {
try {
String pluginGroup = pcft.getPluginGroup();
if(pcft.versionMatches(classLoader)){
if (pluginGroup != null) {
fallbackMap.put(pluginGroup, null);
}
reduced.add(pcft);
} else if(pcft.isFallbackPlugin()){
if (pluginGroup != null && !fallbackMap.containsKey(pluginGroup)) {
fallbackMap.put(pluginGroup, pcft);
}
}
} catch (Exception e) {
LOGGER.warning("Error evaluating aplicability of plugin", e);
}
}
for (PluginClassFileTransformer pcft: fallbackMap.values()) {
if (pcft != null) {
reduced.add(pcft);
}
}
return reduced;
}
/**
* Every classloader should be initialized. Usually if anything interesting happens,
* it is initialized during plugin initialization process. However, some plugins (e.g. Hotswapper)
* are triggered during classloader initialization process itself (@Init on static method). In this case,
* the plugin will be never invoked, until the classloader initialization is invoked from here.
*
* Schedule with some timeout to allow standard plugin initialization process to precede.
*
* @param classLoader the classloader to which this transformation is associated
* @param protectionDomain associated protection domain (if any)
*/
protected void ensureClassLoaderInitialized(final ClassLoader classLoader, final ProtectionDomain protectionDomain) {
if (!seenClassLoaders.containsKey(classLoader)) {
seenClassLoaders.put(classLoader, null);
if (classLoader == null) {
// directly init null (bootstrap) classloader
PluginManager.getInstance().initClassLoader(null, protectionDomain);
} else {
// ensure the classloader should not be excluded
if (shouldScheduleClassLoader(classLoader)) {
// schedule the excecution
PluginManager.getInstance().getScheduler().scheduleCommand(new Command() {
@Override
public void executeCommand() {
PluginManager.getInstance().initClassLoader(classLoader, protectionDomain);
}
@Override
public String toString() {
return "executeCommand: initClassLoader(" + classLoader + ")";
}
}, 1000);
}
}
}
}
private boolean shouldScheduleClassLoader(final ClassLoader classLoader) {
String name = classLoader.getClass().getName();
if (excludedClassLoaders.contains(name)) {
return false;
}
if (excludedClassLoaderPatterns != null) {
for (Pattern pattern : excludedClassLoaderPatterns) {
if (pattern.matcher(name).matches()) {
return false;
}
}
}
return true;
}
/**
* Transform type to ^regexp$ form - match only whole pattern.
*
* @param registeredType type
* @return
*/
protected String normalizeTypeRegexp(String registeredType) {
String regexp = registeredType;
if (!registeredType.startsWith("^")){
regexp = "^" + regexp;
}
if (!registeredType.endsWith("$")){
regexp = regexp + "$";
}
return regexp;
}
}