MultiClassLoader.java
/*
** Module : MultiClassLoader.java
** Abstract : Provides a custom class loader which can manage multiple jars.
**
** Copyright (c) 2011-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------Description----------------------------------
** 001 CA 20111130 Initial version.
** 002 SVL 20130822 On construction this class loader creates InMemoryClassLoader as the parent.
** 003 CA 20140213 Added appendToClassPathForInstrumentation, to allow profilers to be added via
** the Hot Spot VM.
** 004 ECF 20150413 Replaced InMemoryClassLoader with AsmClassLoader in the chain of delegates.
** 005 ECF 20150828 Javadoc cleanup.
** 006 IAS 20150403 Fixed managerLibsDir declaration and initialization.
** 007 VVT 20200924 Fixed: logging in class initialization section causes recursion,
** See. #4527. Also some code polishing: missing annotations added etc.
** 008 SBI 20210414 Changed usages of JarClassLoader to simplify the code.
** 009 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 010 GBB 20230517 Remove use of CentralLogger in static initializer.
** 011 GBB 20230825 checkCallerAbort moved to SecurityUtil & calls updated.
** 012 GBB 20240404 Methods resolveJarName and prettyName moved to JarUtil.
** Static initializer code to JarUtil.resolveClasspathJars to be reused.
** classpathJars type changed from Set to List.
** 013 ICP 20240704 Fixed findResource to ensure that the loaded jars are correctly searched.
** ICP 20240705 Added static method getClassLoader() to retrieve the MultiClassLoader
** from the current thread's context class loader.
** Fixed method of obtaining the class loader.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.classloader;
import com.goldencode.asm.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;
/**
* This class provides a container for managing class loaders for multiple
* jars. When determining the class loader for a file, first it searches for
* a managed jar which handles that file. If it was found, then it delegates
* the request to it; else, it delegates the request to this class loader's
* parent.
* <p>
* To use this class loader, set this property when starting the server:
* <code>-Djava.system.class.loader=com.goldencode.p2j.classloader.MultiClassLoader</code>
*/
public class MultiClassLoader
extends ClassLoader
{
/** logger */
private static final CentralLogger LOG = CentralLogger.get(MultiClassLoader.class.getName(), false, false);
/** Map containing as key the jar name and as value its associated class loader. */
private final Map<String, JarClassLoader> loaders = new HashMap<>();
/** Map each class to the classloader which handles it. */
private final Map<String, JarClassLoader> jclByClasses = new HashMap<>();
/**
* Managed customer libraries directory path. If <code>null</code> then
* it hasn't been read yet, otherwise it is contained in the zero element
* of the array.
*/
private static volatile String managedLibsDir = null;
/** A set containing all jars added to the classpath when the server was started. */
private static final List<String> classpathJars = JarUtil.resolveClasspathJars(null);
/**
* C'tor. Instantiate the class loader by linking it with its parent.
*
* @param parent
* Parent class loader.
*/
public MultiClassLoader(ClassLoader parent)
{
// class loaders will be chained: MultiClassLoader -> AsmClassLoader -> standard class
// loader
super(new AsmClassLoader(parent));
}
/**
* Check if the given class is handled by a custom jar loader for the given
* jar.
*
* @param clazz
* The class object.
* @param jarName
* The jar name.
*
* @return See above.
*/
public static boolean handledByJar(Class<?> clazz, String jarName)
{
return handledByJar(clazz.getName(), jarName);
}
/**
* Check if the given class is handled by a custom jar loader for the given
* jar.
*
* @param className
* The name of the class to be checked.
* @param jarName
* The jar name.
*
* @return See above.
*/
public static boolean handledByJar(String className, String jarName)
{
jarName = JarUtil.resolveJarName(LOG, jarName, false);
if (jarName == null)
{
return false;
}
ClassLoader scl = getClassLoader();
if (!(scl instanceof MultiClassLoader))
{
return false;
}
MultiClassLoader mcl = (MultiClassLoader) scl;
JarClassLoader jcl;
synchronized (mcl)
{
jcl = mcl.loaders.get(jarName);
}
if (jcl != null)
{
return jcl.containsClass(className);
}
return false;
}
/**
* Register the given jar to be handled by its own class loader.
*
* @param jarName
* The jar name.
*
* @return {@link ErrorCode#SUCCESS} if the loading was done. Else,
* it returns an {@link ErrorCode error code}.
*
* @throws RestrictedUseException
* if called improperly.
*/
public static ErrorCode addJarLoader(String jarName)
throws RestrictedUseException
{
SecurityUtil.checkCallerAbort(new String[]{
"com.goldencode.p2j.admin.AdminServerImpl.registerJar",
"com.goldencode.p2j.main.StandardServer.initClassLoaders"},
"com.goldencode.p2j.classloader.MultiClassLoader");
ClassLoader scl = getClassLoader();
if (!(scl instanceof MultiClassLoader))
{
return ErrorCode.BAD_LOADER;
}
return ((MultiClassLoader) scl).addJarLoaderImpl(jarName, false);
}
/**
* Remove the classloader associated with the given jar.
*
* @param jarName
* The jar name.
*
* @return {@link ErrorCode#SUCCESS} if the deregistration was done. Else,
* it returns an {@link ErrorCode error code}.
*
* @throws RestrictedUseException
* if called improperly.
*/
public static ErrorCode removeJarLoader(String jarName)
throws RestrictedUseException
{
SecurityUtil.checkCallerAbort(new String[]{
"com.goldencode.p2j.admin.AdminServerImpl.registerJar",
"com.goldencode.p2j.admin.AdminServerImpl.deregisterJar"},
"com.goldencode.p2j.classloader.MultiClassLoader");
ClassLoader scl = getClassLoader();
if (!(scl instanceof MultiClassLoader))
{
return ErrorCode.BAD_LOADER;
}
return ((MultiClassLoader) scl).removeJarLoaderImpl(jarName);
}
/**
* Get the managed customer libs folder. First, it checks managed.libs.dir
* system property, then, as a fallback, it checks the configuration
* directory (path is <code>/server/default/managed-libs-dir</code> or
* <code>/server/<serverID>/managed-libs-dir</code>).
*
* @return managed customer libs folder or <code>null</code> if it is not
* specified.
*/
public static String managedCustomerLibs()
{
// quick check
if (managedLibsDir == null)
{
synchronized (MultiClassLoader.class)
{
if (managedLibsDir == null)
{
String cl = System.getProperty("managed.libs.dir", null);
if (cl == null)
{
DirectoryService ds = DirectoryService.getInstance();
cl = Utils.getDirectoryNodeString(ds,
"managed-libs-dir",
null,
false);
}
if (cl != null && !cl.endsWith(File.separator))
{
cl = cl + File.separator;
}
managedLibsDir = cl;
}
}
}
return managedLibsDir;
}
/**
* List all classes in the given jar. The classes will not be initialized.
*
* @param jarName
* The jar name.
*
* @return The list of classes in this jar or <code>null</code> in case
* of error.
*/
public static ClassInfo[] listClasses(String jarName)
{
ClassLoader cl = getClassLoader();
jarName = JarUtil.resolveJarName(LOG, jarName, false);
if (jarName == null || !(cl instanceof MultiClassLoader))
{
return null;
}
MultiClassLoader mcl = (MultiClassLoader) cl;
JarClassLoader jcl;
synchronized (mcl)
{
jcl = mcl.loaders.get(jarName);
}
if (jcl != null)
{
// jar is already loaded
return jcl.listClasses();
}
// load jar, get the list of classes, close jar
JarClassLoader loader = JarClassLoader.createJarClassLoader(jarName, cl.getParent());
if (loader == null)
{
return null;
}
ClassInfo[] res = null;
try
{
res = loader.listClasses();
}
finally
{
loader.closeLoader();
}
return res;
}
/**
* List the jars handled by custom class loaders.
*
* @return See above.
*/
public static String[] listRegisteredJars()
{
ClassLoader cl = getClassLoader();
if (!(cl instanceof MultiClassLoader))
{
return new String[0];
}
MultiClassLoader mcl = (MultiClassLoader) cl;
Set<String> names = new TreeSet<>();
synchronized (mcl)
{
for (String name : mcl.loaders.keySet())
{
names.add(JarUtil.prettyName(name));
}
}
return names.toArray(new String[0]);
}
/**
* List the jars which may be handled by custom class loaders (they reside
* in the given path, are not yet handled by custom class loaders and are
* not in the classpath).
* <p>
* The unregistered jars are searched in the customer_libs property
* specified at server startup. If it was not specified, then it will
* return an empty list.
*
* @return See above.
*/
public static String[] listUnregisteredJars()
{
String customerLibs = managedCustomerLibs();
if (customerLibs == null)
return new String[0];
File f = new File(customerLibs);
ClassLoader cl = getClassLoader();
if (!(cl instanceof MultiClassLoader) ||
!f.exists() ||
!f.isDirectory())
{
return new String[0];
}
MultiClassLoader mcl = (MultiClassLoader) cl;
Set<String> registered;
synchronized (mcl)
{
registered = new TreeSet<>(mcl.loaders.keySet());
}
Set<String> unregs = new TreeSet<>();
File[] files = f.listFiles();
for (File file : files)
{
String name = file.getPath();
String jarName = JarUtil.resolveJarName(LOG, name, true);
if (file.isDirectory() ||
!file.getName().endsWith(".jar") ||
classpathJars.contains(jarName) ||
registered.contains(jarName))
{
continue;
}
unregs.add(JarUtil.prettyName(name));
}
return unregs.toArray(new String[unregs.size()]);
}
/**
* Finds the resource with the given name. First it searches the loaded
* jars; if not found, delegates the search to the parent class loader.
*
* @param name
* The resource name
*
* @return A <tt>URL</tt> object for reading the resource, or
* <tt>null</tt> if the resource could not be found
*/
@Override
protected synchronized URL findResource(String name)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Searching for resource " + name);
}
// search in loaders first
URL url = null;
for (String pathToJar : classpathJars)
{
JarClassLoader jcl = loaders.get(pathToJar);
if (jcl != null && jcl.containsResource(name))
{
url = jcl.getResource(name);
break;
}
}
// delegate to parent if not found
return url == null ? getParent().getResource(name) : url;
}
/**
* Find and load the class with the given name. If this is in one of the
* registered Jar class loaders, it will delegate the loading to it. Else,
* the search is delegated to the parent class loader.
*
* @return The {@link Class} object.
*
* @throws ClassNotFoundException
* If the class can not be found or class linkage has error
* occurred.
*/
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Searching for class " + name);
}
Class<?> clazz = getClass(name);
try
{
return clazz == null ? super.findClass(name) : clazz;
}
catch (NoClassDefFoundError e)
{
throw new ClassNotFoundException("Linkage error for class " + name,
e);
}
}
/**
* Find and load the class with the given name. If this is in one of the
* registered Jar class loaders, it will delegate the loading to it. Else,
* the search is delegated to the parent class loader.
*
* @return The {@link Class} object.
*
* @throws ClassNotFoundException
* If the class can not be found or class linkage has error
* occurred.
*/
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Loading class " + name);
}
try
{
Class<?> clazz;
try
{
clazz = getParent().loadClass(name);
}
catch (ClassNotFoundException e)
{
clazz = findClass(name);
}
return clazz == null ? super.loadClass(name) : clazz;
}
catch (NoClassDefFoundError e)
{
throw new ClassNotFoundException("Linkage error for class " + name,
e);
}
}
/**
* Find and load the class with the given name. If this is in one of the
* registered Jar class loaders, it will delegate the loading to it. Else,
* the search is delegated to the parent class loader.
*
* @param name
* The class name
*
* @return The {@link Class} object, or <code>null</code> if this class
* does not belong to any of the registered jars.
*
* @throws ClassNotFoundException
* If the class belongs must be handled by a {@link JarClassLoader}
* but can not be loaded.
*/
public synchronized Class<?> getClass(String name)
throws ClassNotFoundException
{
JarClassLoader jcl = jclByClasses.get(name);
if (jcl == null)
{
// check the jar loaders
for (String pathToJar : loaders.keySet())
{
JarClassLoader jar = loaders.get(pathToJar);
if (jar != null && jar.containsClass(name))
{
if (jcl != null)
{
// class found in more than one jar, log inf
String jar1 = jar.getJarName();
String jar2 = jcl.getJarName();
// JVM says that only the first returned reference is
// handled - if more, I don't care
if (LOG.isLoggable(Level.WARNING))
{
LOG.warning("Class " + name +
" is defined in both " +
jar1 + " and " + jar2 + " jars !");
}
break;
}
jcl = jar;
}
}
if (jcl != null)
{
jclByClasses.put(name, jcl);
}
}
return jcl == null ? null : jcl.loadClass(name);
}
/**
* To support dynamic additions to the class path at runtime, the class loader needs to define
* this private method. This will be invoked from the native VM code, to load a jar.
* <p>
* This method does not have to be public.
*
* @param path
* The full path of the jar to be appended.
*/
@SuppressWarnings("unused")
private void appendToClassPathForInstrumentation(String path)
{
addJarLoaderImpl(path, true);
}
/**
* Register the given jar to be handled by its own class loader.
*
* @param jarName
* The jar name.
* @param fullPath
* <code>true</code> if the <code>jarName</code> contains also
* the jar's path. When <code>false</code>, it is assumed that
* the path is the one specified in the "customer_libs" system
* property.
*
* @return {@link ErrorCode#SUCCESS} if the loading was done. Else,
* it returns an {@link ErrorCode error code}.
*/
private synchronized ErrorCode addJarLoaderImpl(String jarName, boolean fullPath)
{
jarName = JarUtil.resolveJarName(LOG, jarName, fullPath);
if (jarName == null)
{
return ErrorCode.NOT_FOUND;
}
if (classpathJars.contains(jarName))
{
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO,
"Could not add jar loader for jar " + jarName +
" - it is already added to classpath.");
}
return ErrorCode.IN_CLASSPATH;
}
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO,
"Adding jar loader for jar " + jarName);
}
if (loaders.containsKey(jarName))
{
// jar already loaded, must remove first
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Jar loader for jar " + jarName +
"is already loaded - it will be removed before adding.");
}
ErrorCode er = removeJarLoaderImpl(jarName);
if (er != ErrorCode.SUCCESS)
{
return er;
}
}
JarClassLoader loader = JarClassLoader.getOrCreateCachedJarClassLoader(jarName, getParent());
if (loader != null)
{
loaders.put(jarName, loader);
}
else
{
// jar can not be initialized
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE,
"Could not initialize loader for jar " + jarName);
}
return ErrorCode.INIT_ERROR;
}
return ErrorCode.SUCCESS;
}
/**
* Remove the classloader associated with the given jar.
*
* @param jarName
* The jar name.
*
* @return {@link ErrorCode#SUCCESS} if the deregistration was done. Else,
* it returns an {@link ErrorCode error code}.
*/
private synchronized ErrorCode removeJarLoaderImpl(String jarName)
{
jarName = JarUtil.resolveJarName(LOG, jarName, false);
if (jarName == null)
{
return ErrorCode.NOT_FOUND;
}
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO, "Removing jar loader for jar " + jarName);
}
JarClassLoader jar = loaders.get(jarName);
if (jar == null)
{
// jar can not be found
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Can not remove jar loader for jar " + jarName +
": the jar is not registered with a class loader !");
}
return ErrorCode.NOT_REGISTERED;
}
if (!jar.closeLoader())
{
// jar can not be closed
return ErrorCode.CLOSE_ERROR;
}
loaders.remove(jarName);
// remove all cached classes
Iterator<JarClassLoader> itr = jclByClasses.values().iterator();
while (itr.hasNext())
{
JarClassLoader jcl = itr.next();
if (jcl == jar)
itr.remove();
}
return ErrorCode.SUCCESS;
}
/**
* Utility method to obtain the MultiClassLoader instance from the current thread's context
* class loader. If no MultiClassLoader is found in the thread hierarchy, it returns the
* system class loader.
*
* @return The MultiClassLoader instance if found, otherwise the system class loader.
*/
public static ClassLoader getClassLoader()
{
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
while (!(classLoader instanceof MultiClassLoader) &&
classLoader.getParent() != null)
{
classLoader = classLoader.getParent();
}
if (!(classLoader instanceof MultiClassLoader))
{
classLoader = ClassLoader.getSystemClassLoader();
}
return classLoader;
}
}