package org.hotswap.agent.config; import org.hotswap.agent.HotswapAgent; import org.hotswap.agent.annotation.Plugin; import org.hotswap.agent.annotation.handler.AnnotationProcessor; import org.hotswap.agent.logging.AgentLogger; import org.hotswap.agent.util.classloader.ClassLoaderPatcher; import org.hotswap.agent.util.scanner.ClassPathAnnotationScanner; import org.hotswap.agent.util.scanner.ClassPathScanner; import java.util.*; /** * Registry to support plugin manager. * * @author Jiri Bubnik */ public class PluginRegistry { private static AgentLogger LOGGER = AgentLogger.getLogger(PluginRegistry.class); // plugin class -> Map (ClassLoader -> Plugin instance) protected Map<Class, Map<ClassLoader, Object>> registeredPlugins = Collections.synchronizedMap(new HashMap<Class, Map<ClassLoader, Object>>()); /** * Returns map of all registered plugins. * * @return map plugin class -> Map (ClassLoader -> Plugin instance) */ public Map<Class, Map<ClassLoader, Object>> getRegisteredPlugins() { return registeredPlugins; } // plugin manager instance private PluginManager pluginManager; // scanner to search for plugins private ClassPathAnnotationScanner annotationScanner; public void setAnnotationScanner(ClassPathAnnotationScanner annotationScanner) { this.annotationScanner = annotationScanner; } // processor to resolve plugin annotations protected AnnotationProcessor annotationProcessor; public void setAnnotationProcessor(AnnotationProcessor annotationProcessor) { this.annotationProcessor = annotationProcessor; } // copy plugin classes from application classloader to the agent classloader private ClassLoaderPatcher classLoaderPatcher; public void setClassLoaderPatcher(ClassLoaderPatcher classLoaderPatcher) { this.classLoaderPatcher = classLoaderPatcher; } /** * Create an instanec of plugin registry and initialize scanner and processor. */ public PluginRegistry(PluginManager pluginManager, ClassLoaderPatcher classLoaderPatcher) { this.pluginManager = pluginManager; this.classLoaderPatcher = classLoaderPatcher; annotationScanner = new ClassPathAnnotationScanner(Plugin.class.getName(), new ClassPathScanner()); annotationProcessor = new AnnotationProcessor(pluginManager); } /** * Scan for plugins by @Plugin annotation on PLUGIN_PATH and process plugin annotations. * * @param classLoader classloader to resolve plugin package. This will be used by annotation scanner. * @param pluginPackage the package to be searched (e.g. org.agent.hotswap.plugin) */ public void scanPlugins(ClassLoader classLoader, String pluginPackage) { String pluginPath = pluginPackage.replace(".", "/"); ClassLoader agentClassLoader = getClass().getClassLoader(); try { List<String> discoveredPlugins = annotationScanner.scanPlugins(classLoader, pluginPath); List<String> discoveredPluginNames = new ArrayList<String>(); // Plugin class must be always defined directly in the agent classloader, otherwise it will not be available // to the instrumentation process. Copy the definition using patcher if (discoveredPlugins.size() > 0 && agentClassLoader != classLoader) { classLoaderPatcher.patch(classLoader, pluginPath, agentClassLoader, null); } for (String discoveredPlugin : discoveredPlugins) { Class pluginClass = Class.forName(discoveredPlugin, true, agentClassLoader); Plugin pluginAnnotation = (Plugin) pluginClass.getAnnotation(Plugin.class); if (pluginAnnotation == null) { LOGGER.error("Scanner discovered plugin class {} which does not contain @Plugin annotation.", pluginClass); continue; } String pluginName = pluginAnnotation.name(); if (HotswapAgent.isPluginDisabled(pluginName)) { LOGGER.debug("Plugin {} is disabled, skipping...", pluginName); continue; } // check for duplicate plugin definition. It may happen if class directory AND the JAR file // are both available. if (registeredPlugins.containsKey(pluginClass)) continue; registeredPlugins.put(pluginClass, Collections.synchronizedMap(new HashMap<ClassLoader, Object>())); if (annotationProcessor.processAnnotations(pluginClass, pluginClass)) { LOGGER.debug("Plugin registered {}.", pluginClass); } else { LOGGER.error("Error processing annotations for plugin {}. Plugin was unregistered.", pluginClass); registeredPlugins.remove(pluginClass); } discoveredPluginNames.add(pluginName); } LOGGER.info("Discovered plugins: " + Arrays.toString(discoveredPluginNames.toArray())); } catch (Exception e) { LOGGER.error("Error in plugin initial processing for plugin package '{}'", e, pluginPackage); } } /** * Init a plugin (create new plugin instance) in a application classloader. * Each classloader may contain only one instance of a plugin. * * @param pluginClass class of plugin to instantiate * @param appClassLoader target application classloader * @return the new plugin instance or null if plugin is disabled. */ public Object initializePlugin(String pluginClass, ClassLoader appClassLoader) { if (appClassLoader == null) throw new IllegalArgumentException("Cannot initialize plugin '" + pluginClass + "', appClassLoader is null."); // ensure classloader initialized pluginManager.initClassLoader(appClassLoader); Class<Object> clazz = getPluginClass(pluginClass); // skip if the plugin is disabled if (pluginManager.getPluginConfiguration(appClassLoader).isDisabledPlugin(clazz)) { LOGGER.debug("Plugin {} disabled in classloader {}.", clazz, appClassLoader ); return null; } // already initialized in this or parent classloader if (hasPlugin(clazz, appClassLoader, false)) { LOGGER.debug("Plugin {} already initialized in parent classloader of {}.", clazz, appClassLoader ); return getPlugin(clazz, appClassLoader); } Object pluginInstance = instantiate(clazz); registeredPlugins.get(clazz).put(appClassLoader, pluginInstance); if (annotationProcessor.processAnnotations(pluginInstance)) { LOGGER.info("Plugin '{}' initialized in ClassLoader '{}'.", pluginClass, appClassLoader); } else { LOGGER.error("Plugin '{}' NOT initialized in ClassLoader '{}', error while processing annotations.", pluginClass, appClassLoader); registeredPlugins.get(clazz).remove(appClassLoader); } return pluginInstance; } /** * Returns plugin instance by it's type and classLoader. * * @param pluginClass type of the plugin * @param classLoader classloader of the plugin * @param <T> type of the plugin to return correct instance. * @return the plugin * @throws IllegalArgumentException if classLoader not initialized or plugin not found */ public <T> T getPlugin(Class<T> pluginClass, ClassLoader classLoader) { if (registeredPlugins.isEmpty()) { throw new IllegalStateException("No plugin initialized. " + "The Hotswap Agent JAR must NOT be in app classloader (only registered as --javaagent: startup parameter). " + "Please check your mapPreviousState."); } if (!registeredPlugins.containsKey(pluginClass)) throw new IllegalArgumentException(String.format("Plugin %s is not known to the registry.", pluginClass)); Map<ClassLoader, Object> pluginInstances = registeredPlugins.get(pluginClass); synchronized(pluginInstances) { for (Map.Entry<ClassLoader, Object> registeredClassLoaderEntry : pluginInstances.entrySet()) { if (isParentClassLoader(registeredClassLoaderEntry.getKey(), classLoader)) { //noinspection unchecked return (T) registeredClassLoaderEntry.getValue(); } } } // not found throw new IllegalArgumentException(String.format("Plugin %s is not initialized in classloader %s.", pluginClass, classLoader)); } /** * Check if plugin is initialized in classLoader. * * @param pluginClass type of the plugin * @param classLoader classloader of the plugin * @param checkParent for parent classloaders as well? * @return true/false */ public boolean hasPlugin(Class<?> pluginClass, ClassLoader classLoader, boolean checkParent) { if (!registeredPlugins.containsKey(pluginClass)) return false; Map<ClassLoader, Object> pluginInstances = registeredPlugins.get(pluginClass); synchronized (pluginInstances) { for (Map.Entry<ClassLoader, Object> registeredClassLoaderEntry : pluginInstances.entrySet()) { if (checkParent && isParentClassLoader(registeredClassLoaderEntry.getKey(), classLoader)) { return true; } else if (registeredClassLoaderEntry.getKey().equals(classLoader)) { return true; } } } return false; } /** * Search for the plugin in the registry and return associated classloader. * * @param plugin existing plugin * @return the classloader this plugin is associated with */ public ClassLoader getAppClassLoader(Object plugin) { // search with for loop. Maybe performance improvement to create reverse map if this is used heavily Class<Object> clazz = getPluginClass(plugin.getClass().getName()); Map<ClassLoader, Object> pluginInstances = registeredPlugins.get(clazz); if (pluginInstances != null) { synchronized(pluginInstances) { for (Map.Entry<ClassLoader, Object> entry : pluginInstances.entrySet()) { if (entry.getValue().equals(plugin)) return entry.getKey(); } } } throw new IllegalArgumentException("Plugin not found in the registry " + plugin); } // resolve class in this classloader - plugin class should be always only in the same classloader // as the plugin manager. protected Class<Object> getPluginClass(String pluginClass) { try { // noinspection unchecked return (Class<Object>) getClass().getClassLoader().loadClass(pluginClass); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Plugin class not found " + pluginClass, e); } } // check if parentClassLoader is parent of classLoader private boolean isParentClassLoader(ClassLoader parentClassLoader, ClassLoader classLoader) { if (parentClassLoader.equals(classLoader)) return true; else if (classLoader.getParent() != null) return isParentClassLoader(parentClassLoader, classLoader.getParent()); else return false; } /** * Create a new instance of the plugin. * * @param plugin plugin class * @return new instance or null if instantiation fail. */ protected Object instantiate(Class<Object> plugin) { try { return plugin.newInstance(); } catch (InstantiationException e) { LOGGER.error("Error instantiating plugin: " + plugin.getClass().getName(), e); } catch (IllegalAccessException e) { LOGGER.error("Plugin: " + plugin.getClass().getName() + " does not contain public no param constructor", e); } return null; } /** * Remove all registered plugins for a classloader. * @param classLoader classloader to cleanup */ public void closeClassLoader(ClassLoader classLoader) { LOGGER.debug("Closing classloader {}.", classLoader); synchronized (registeredPlugins) { for (Map<ClassLoader, Object> plugins : registeredPlugins.values()) { plugins.remove(classLoader); } } } }