/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.pc.plugin; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.clientapi.agent.PluginContainerException; import org.rhq.core.clientapi.agent.metadata.PluginDependencyGraph; import org.rhq.core.domain.resource.ClassLoaderType; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.resource.ResourceType; import org.rhq.core.pc.PluginContainer; import org.rhq.core.pc.inventory.InventoryManager; import org.rhq.core.pc.inventory.ResourceContainer; /** * Manages the classloaders created and used by the plugin container and all plugins/resources. * * @author John Mazzitelli */ public class ClassLoaderManager { private final Log log = LogFactory.getLog(ClassLoaderManager.class); /** * Directory where temporary files can be stored. Used to extract jars embedded in plugin jars. */ private final File tmpDir; /** * Indicates what plugins are deployed and their hierarchies. */ private final PluginDependencyGraph pluginDependencyGraph; /** * Provides a map keyed on plugin name whose values are the URLs to those plugin jars. */ private final Map<String, URL> pluginNamesUrls; /** * The parent classloader for those classloaders at the top of the classloader hierarchy. */ private final ClassLoader rootClassLoader; /** * These are the classloaders that are built that follow the plugin hierarchy as found in the * dependency graph. These classloaders follow the hierarchy as defined by the plugin descriptors * and their required dependencies. This map is keyed on plugin name. * See {@link #obtainPluginClassLoader(String)}. */ private final Map<String, ClassLoader> pluginClassLoaders; /** * These are the classloaders that are to be used to load discovery components whose discovered * resources have parents that contain connection classes necessary for the discovery components * to do their job. This map is keyed on a hash calculated from plugin name and parent classloader. * See {@link #obtainDiscoveryClassLoader(String, ClassLoader)}. */ private final Map<String, ClassLoader> discoveryClassLoaders; /** * Contains all classloaders for all individual resources that got classloaders created for it. * The map is keyed on a hash built from data belonging to a resource and its parent resource. * See {@link #obtainResourceClassLoader(Resource, ResourceContainer, List)}. */ private final Map<CanonicalResourceKey, ClassLoader> resourceClassLoaders; /** * If <code>true</code>, then this manager will create instances of classloaders for those * individual resources that require it. If <code>false</code>, this manager will never create * individual classloaders for resources; it will only ever obtain plugin classloaders for resources. * This means that <code>false</code> will force {@link #obtainResourceClassLoader(Resource, ResourceContainer, List)} * to only ever return plugin classloaders. */ private final boolean createResourceClassLoaders; /** * Creates the object that will manage all classloaders for the plugins deployed in the given plugin deployment graph. * * @param pluginNamesUrls maps a plugin name with the URL to that plugin's jar file * @param graph the graph that provides plugin dependency information for all plugins that are deployed * @param rootClassLoader the classloader at the top of the classloader hierarchy to be used as the parent classloader * for those classloaders that are not children to other shared/resource classloaders. * @param tmpDir where the classloaders can write out the jars that are embedded in the plugin jars * @param createResourceClassLoaders if <code>true</code>, the classloader manager will create resource classloader * instances when appropriate. If <code>false</code>, this classloader manager * will never create classloaders on a per-resource instance basis. It will only * ever create and return plugin classloaders. This will be <code>false</code> when * the plugin container is running embedded inside a managed resource and that * managed resource will provide the necessary client jars via the root classloader. */ public ClassLoaderManager(Map<String, URL> pluginNamesUrls, PluginDependencyGraph graph, ClassLoader rootClassLoader, File tmpDir, boolean createResourceClassLoaders) { this.rootClassLoader = rootClassLoader; this.pluginClassLoaders = new HashMap<String, ClassLoader>(); this.resourceClassLoaders = new HashMap<CanonicalResourceKey, ClassLoader>(); this.discoveryClassLoaders = new HashMap<String, ClassLoader>(); this.pluginNamesUrls = pluginNamesUrls; this.pluginDependencyGraph = graph; this.tmpDir = tmpDir; this.createResourceClassLoaders = createResourceClassLoaders; } /** * Cleans up this object and all classloaders it has created. */ public void destroy() { Set<PluginClassLoader> toDestroyClassLoaders = new HashSet<PluginClassLoader>(); // destroy any resource classloaders we've created for (ClassLoader doomedCL : getUniqueResourceClassLoaders()) { if (doomedCL instanceof PluginClassLoader) { toDestroyClassLoaders.add((PluginClassLoader) doomedCL); } } resourceClassLoaders.clear(); // destroy any discovery classloaders we've created for (ClassLoader doomedCL : getUniqueDiscoveryClassLoaders()) { if (doomedCL instanceof PluginClassLoader) { toDestroyClassLoaders.add((PluginClassLoader) doomedCL); } } discoveryClassLoaders.clear(); // destroy any plugin classloaders we've created for (ClassLoader doomedCL : getUniquePluginClassLoaders()) { if (doomedCL instanceof PluginClassLoader) { toDestroyClassLoaders.add((PluginClassLoader) doomedCL); } } pluginClassLoaders.clear(); for (PluginClassLoader pluginClassLoader : toDestroyClassLoaders) { pluginClassLoader.destroy(); } } @Override public String toString() { Set<ClassLoader> classLoaders; StringBuilder str = new StringBuilder(this.getClass().getSimpleName()); classLoaders = getUniquePluginClassLoaders(); str.append(" [#plugin CLs=").append(classLoaders.size()); classLoaders.clear(); // help out the GC, clear out the shallow copy container classLoaders = getUniqueDiscoveryClassLoaders(); str.append(", #discovery CLs=").append(classLoaders.size()); classLoaders.clear(); classLoaders = getUniqueResourceClassLoaders(); str.append(", #resource CLs=").append(classLoaders.size()); classLoaders.clear(); str.append(']'); return str.toString(); } /** * Returns the classloader that should be the ancestor (i.e. top most parent) of all plugin classloaders. * * @return the root plugin classloader for all plugins */ public ClassLoader getRootClassLoader() { return this.rootClassLoader; } /** * Returns the graph of all the plugins and their dependencies. * * @return plugin dependency graph */ public PluginDependencyGraph getPluginDependencyGraph() { return pluginDependencyGraph; } /** * Returns a plugin classloader (creating it if necessary) that contains the plugin jar and whose parent * classloader is that of the the classloader for the required (<depends>) plugin. In other words, * this follows the plugin dependency hierarchy as defined in the given * {@link #getPluginDependencyGraph() dependency graph}. * * @param pluginName the plugin whose classloader is to be created * @return the plugin classloader * @throws PluginContainerException */ public synchronized ClassLoader obtainPluginClassLoader(String pluginName) throws PluginContainerException { ClassLoader cl = this.pluginClassLoaders.get(pluginName); if (cl == null) { URL pluginJarUrl = this.pluginNamesUrls.get(pluginName); String useClassesDep = this.pluginDependencyGraph.getUseClassesDependency(pluginName); ClassLoader parentClassLoader; if (useClassesDep != null) { parentClassLoader = obtainPluginClassLoader(useClassesDep); if (log.isDebugEnabled()) { List<String> dependencies = this.pluginDependencyGraph.getPluginDependencies(pluginName); log.debug("Creating classloader for dependent plugin [" + pluginName + "] from URL [" + pluginJarUrl + "] that has the following dependencies: " + dependencies); } } else { parentClassLoader = this.rootClassLoader; if (log.isDebugEnabled()) { log.debug("Creating classloader for independent plugin [" + pluginName + "] from URL [" + pluginJarUrl + ']'); } } cl = createClassLoader(pluginJarUrl, null, parentClassLoader); this.pluginClassLoaders.put(pluginName, cl); } return cl; } /** * Similar to {@link #obtainPluginClassLoader(String)}, however, the classloader to be returned * will have the given parent classloader (as opposed to having parent classloaders that follow the * plugin dependency graph hierarchy). This is used to support loading discovery components where * those discovery components need to use connections to the parent resource in order to perform their * discovery. * * @param pluginName the name of the plugin where the discovery component can be found * @param parentClassLoader the parent classloader of the new classloader being created * @return the new plugin classloader * @throws PluginContainerException */ public synchronized ClassLoader obtainDiscoveryClassLoader(String pluginName, ClassLoader parentClassLoader) throws PluginContainerException { String hash = pluginName + '-' + Integer.toHexString(parentClassLoader.hashCode()); ClassLoader cl = this.discoveryClassLoaders.get(hash); if (cl == null) { URL pluginJarUrl = this.pluginNamesUrls.get(pluginName); if (log.isDebugEnabled()) { log.debug("Creating discovery classloader [" + hash + "] from URL [" + pluginJarUrl + ']'); } cl = createClassLoader(pluginJarUrl, null, parentClassLoader); this.discoveryClassLoaders.put(hash, cl); } return cl; } /** * Returns the classloader that the given resource should use when being invoked. * Note that the parent container should never be <code>null</code>; if it is, the caller should * just assume the resource is the top-level platform and just get that platform resource's * plugin classloader. * * @param resource the resource whose classloader is to be obtained * @param parent the container for the parent of the given resource (must never be <code>null</code>) * @param additionalJars additional jars to put into the classloader * @return the resource's classloader - this will be newly created if this is the first time we've been asked to obtain it * @throws PluginContainerException */ public synchronized ClassLoader obtainResourceClassLoader(Resource resource, ResourceContainer parent, List<URL> additionalJars) throws PluginContainerException { if (resource == null) { throw new PluginContainerException("resource must not be null"); } if (parent == null) { throw new PluginContainerException("parent must not be null"); } CanonicalResourceKey mapKey = new CanonicalResourceKey(resource, parent.getResource()); ClassLoader resourceCL = this.resourceClassLoaders.get(mapKey); if (resourceCL == null) { if (this.createResourceClassLoaders) { ResourceType resourceType = resource.getResourceType(); String resourcePlugin = resourceType.getPlugin(); ClassLoaderType resourceClassLoaderType = resourceType.getClassLoaderType(); ResourceType parentResourceType = parent.getResource().getResourceType(); String parentPlugin = parentResourceType.getPlugin(); ClassLoaderType parentClassLoaderType = parentResourceType.getClassLoaderType(); if (resourcePlugin.equals(parentPlugin)) { // both resource and parent are from the same plugin, resource uses the same CL as its parent resourceCL = parent.getResourceClassLoader(); } else { // resource and parent are from different plugins // Determine if we reached the top of the resource hierarchy (i.e. the parent is the top platform resource) // This will only happen if the classloader type is SHARED and it equals the platform resource. boolean isParentTopPlatform = false; if (parentClassLoaderType == ClassLoaderType.SHARED) { InventoryManager inventoryMgr = PluginContainer.getInstance().getInventoryManager(); isParentTopPlatform = parent.getResource().equals(inventoryMgr.getPlatform()); } if (resourceClassLoaderType == ClassLoaderType.SHARED && parentClassLoaderType == ClassLoaderType.SHARED) { // Both resource and parent are willing to share their classloader. // If we reached the top of the resource hierarchy, our resource's classloader needs only // be its own plugin classloader. // If we are not at the top, the resource is running inside its parent resource and thus needs to have // that parent resource's classloader, but it also needs to have its own classes from its own plugin. // Therefore, in both cases, the resource gets assigned its own plugin classloader. This means that // its plugin must also have the parent plugin as a required dependency. // (e.g. RHQ-Server plugin resource runs inside JBossAS server) // So, yes this if-else does the same, but it is here in case we need to add additional functionality later. if (isParentTopPlatform) { resourceCL = obtainPluginClassLoader(resourcePlugin); } else { resourceCL = obtainPluginClassLoader(resourcePlugin); } } else if (resourceClassLoaderType == ClassLoaderType.INSTANCE && parentClassLoaderType == ClassLoaderType.SHARED) { // Resource wants its own classloader, even though the parent is willing to share its classloader. // If we reached the top of the resource hierarchy, our resource's new classloader needs to follow // up its plugin classloader hierachy. If we are not at the top, the resource is running inside // its parent resource and thus needs to have that parent resource's classloader. if (isParentTopPlatform) { resourceCL = createClassLoader(this.pluginNamesUrls.get(resourcePlugin), additionalJars, obtainPluginClassLoader(resourcePlugin).getParent()); } else { // TODO: fixme - https://bugzilla.redhat.com/show_bug.cgi?id=863449 resourceCL = createClassLoader(this.pluginNamesUrls.get(resourcePlugin), additionalJars, obtainPluginClassLoader(parentPlugin)); } } else if (resourceClassLoaderType == ClassLoaderType.SHARED && parentClassLoaderType == ClassLoaderType.INSTANCE) { // Resource is willing to share its own classloader, but the parent has created its own instance. // So, even though this resource says it can be shared, it really needs to have its own instance // because the parent has its own instance. Therefore, the resource has its own instance of a // classloader whose parent classloader is that of its parent resource (e.g. Hibernate running in JBossAS). URL resourcePluginUrl = this.pluginNamesUrls.get(resourcePlugin); ClassLoader parentClassLoader = parent.getResourceClassLoader(); resourceCL = createClassLoader(resourcePluginUrl, additionalJars, parentClassLoader); } else if (resourceClassLoaderType == ClassLoaderType.INSTANCE && parentClassLoaderType == ClassLoaderType.INSTANCE) { // Both the resource and parent want their own classloader instance. // This is effectively the same as in the SHARED/INSTANCE case above. URL resourcePluginUrl = this.pluginNamesUrls.get(resourcePlugin); ClassLoader parentClassLoader = parent.getResourceClassLoader(); resourceCL = createClassLoader(resourcePluginUrl, additionalJars, parentClassLoader); } else { throw new IllegalStateException("Classloader type was never set. rclt=[" + resourceClassLoaderType + "], pclt=[" + parentClassLoaderType + "]"); } } } else { // The plugin container has told us to not create individual resource classloaders, so just return // the resource's plugin classloader and assume the root classloader will give us the extra classes needed. ResourceType resourceType = resource.getResourceType(); String resourcePlugin = resourceType.getPlugin(); resourceCL = obtainPluginClassLoader(resourcePlugin); } this.resourceClassLoaders.put(mapKey, resourceCL); } return resourceCL; } /** * Returns the total number of plugin classloaders that have been created and managed. * This method is here just to support the plugin container management MBean. * * @return number of plugin classloaders that are currently created and being used */ public synchronized int getNumberOfPluginClassLoaders() { return this.pluginClassLoaders.size(); } /** * Returns the total number of discovery classloaders that have been created and managed. * This method is here just to support the plugin container management MBean. * * @return number of discovery classloaders that are currently created and being used */ public synchronized int getNumberOfDiscoveryClassLoaders() { return this.discoveryClassLoaders.size(); } /** * Returns the total number of resource classloaders that have been created and managed. * This is the count of unique classloader instances that have been created - each resource * classloader could potentially be assigned to multiple resources. * This method is here just to support the plugin container management MBean. * * @return number of unique resource classloaders that are currently created and assigned to resources */ public synchronized int getNumberOfResourceClassLoaders() { Set<ClassLoader> uniqueClassLoaders = getUniqueResourceClassLoaders(); int size = uniqueClassLoaders.size(); uniqueClassLoaders.clear(); // this is a shallow copy, help out the GC by nulling it out return size; } /** * Returns a shallow copy of the plugin classloaders keyed on plugin name. This method is here * just to support the plugin container management MBean. * * Do not use this method to obtain a plugin's classloader, instead, you want to use * {@link #obtainPluginClassLoader(String)}. * * @return all plugin classloaders currently assigned to plugins (will never be <code>null</code>) */ public synchronized Map<String, ClassLoader> getPluginClassLoaders() { return new HashMap<String, ClassLoader>(this.pluginClassLoaders); } /** * Returns a shallow copy of the discovery classloaders keyed on a hash calculated from * plugin name and parent classloader. This method is here just to support the plugin * container management MBean. * * Do not use this method to obtain a discovery classloader, instead, you want to use * {@link #obtainDiscoveryClassLoader(String, ClassLoader)}. * * @return all discovery classloaders currently created (will never be <code>null</code>) */ public synchronized Map<String, ClassLoader> getDiscoveryClassLoaders() { return new HashMap<String, ClassLoader>(this.discoveryClassLoaders); } /** * Returns a shallow copy of the resource classloaders keyed on a canonical keys. * This method is here just to support the plugin container management MBean. * * Do not use this method to obtain a resource's classloader, instead, you want to use * {@link #obtainResourceClassLoader(Resource, ResourceContainer, List)}. * * @return all resource classloaders currently assigned to resources (will never be <code>null</code>) */ public synchronized Map<CanonicalResourceKey, ClassLoader> getResourceClassLoaders() { return new HashMap<CanonicalResourceKey, ClassLoader>(this.resourceClassLoaders); } /** * Returns <code>true</code> if this manager will create instances of classloaders for those * individual Resources that require it, or <code>false</code> if this manager will never create * individual classloaders for Resources (i.e. {@link #obtainResourceClassLoader(Resource, ResourceContainer, List)} * will always just return plugin classloaders). * * @return <code>true</code> if this manager will create instances of classloaders for those * individual Resources that require it, or <code>false</code> if this manager will never create * individual classloaders for Resources (i.e. {@link #obtainResourceClassLoader(Resource, ResourceContainer, List)} * will always just return plugin classloaders) */ public boolean isCreateResourceClassLoaders() { return this.createResourceClassLoaders; } private Set<ClassLoader> getUniquePluginClassLoaders() { return new HashSet<ClassLoader>(this.pluginClassLoaders.values()); } private Set<ClassLoader> getUniqueDiscoveryClassLoaders() { return new HashSet<ClassLoader>(this.discoveryClassLoaders.values()); } private Set<ClassLoader> getUniqueResourceClassLoaders() { return new HashSet<ClassLoader>(this.resourceClassLoaders.values()); } private ClassLoader createClassLoader(URL mainJarUrl, List<URL> additionalJars, ClassLoader parentClassLoader) throws PluginContainerException { ClassLoader classLoader; if (parentClassLoader == null) { parentClassLoader = this.getClass().getClassLoader(); } if (mainJarUrl != null) { // Note that we don't really care if the URL uses "file:" or not, // we just use File to parse the name from the path. String pluginJarName = new File(mainJarUrl.getPath()).getName(); if (additionalJars == null || additionalJars.size() == 0) { classLoader = PluginClassLoader.create(pluginJarName, mainJarUrl, true, parentClassLoader, this.tmpDir); } else { List<URL> allJars = new ArrayList<URL>(additionalJars.size() + 1); allJars.add(mainJarUrl); allJars.addAll(additionalJars); classLoader = PluginClassLoader.create(pluginJarName, allJars.toArray(new URL[allJars.size()]), true, parentClassLoader, this.tmpDir); } if (log.isDebugEnabled()) { log.debug("Created classloader for plugin jar [" + mainJarUrl + "] with additional jars [" + additionalJars + "]"); } } else { // this is mainly to support tests classLoader = parentClassLoader; } return classLoader; } }