tomcat类加载源码分析
在tomcat里某个应用中,每个应用包含1个类加载器WebappClassLoader,该应用的类都通过该类加载器加载,其loadClass方法如下:
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return (loadClass(name, false));
}
@Override
public synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding J2SE classes
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = Class.forName(name, false, loader);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = Class.forName(name, false, loader);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
throw new ClassNotFoundException(name);
}
step1:首先是在该WebappClassLoader缓存的类中检查要加载的类有没有缓存过,会调用findLoadedClass0方法:
protected Class<?> findLoadedClass0(String name) {
ResourceEntry entry = resourceEntries.get(name);
if (entry != null) {
return entry.loadedClass;
}
return (null); // FIXME - findLoadedResource()
}
在WebappClassLoader中,缓存的类保存在resourceEntries这个成员里,它是1个map结构,key为类的名字,value为ResourceEntry:
protected HashMap<String, ResourceEntry> resourceEntries = new HashMap<String, ResourceEntry>();
ResourceEntry的成员如下:
成员 | 类型 | 说明 |
---|---|---|
lastModified | long | 上次更新时间,用于热加载功能 |
binaryContent | byte[] | 类的字节数组 |
loadedClass | Class<?> | 保存该类的类实例 |
source | URL | URL source from where the object was loaded. |
codeBase | URL | URL of the codebase from where the object was loaded. |
manifest | Manifest | Manifest (if the resource was loaded from a JAR). |
certificates | Certificate[] | Certificates (if the resource was loaded from a JAR). |
以我部署的web.war项目为例,对于/WEB-INF/classes/下的类,如自定义的com.meli.inc.tomcat.web.HelloController类,其ResourceEntry如下:
HelloController.png对于/WEB-INF/lib/*.jar下的类,如org.springframework.context.i18n.LocaleContext,其ResourceEntry如下:
LocaleContext.png其source成员的file和path如下:
file:/Users/cangxing/Documents/Study/tomcat/Tomcat/tomcat-7.0.42-sourcecode/webapps/web/WEB-INF/lib/spring-context-4.0.2.RELEASE.jar!/org/springframework/context/i18n/LocaleContext.class
以上ResourceEntry截图中,是在调用defineClass方法之前各个变量的值,loadedClass为null,实际上,在调用defineClass方法后,会把class字节码文件字节流转换为运行时class类对象,即loadedClass会被赋值为Class<?>类型的该类类对象,然后把ResourceEntry其它成员置为null,因为该类已经被加载到了内存中,即loadedClass,其它成员没有用了,置为null将其gc掉。
step2:然后是检查jvm的类加载器中有没有缓存过该类的加载器,会调用父类ClassLoader的findLoadedClass方法:
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
在ClassLoader类中,findLoadedClass0为本地方法。
step3:然后用系统类加载器加载该类,防止J2SE的基础类被覆盖,会调用系统类加载器的loadClass方法。
step4:然后在本地仓库中寻找该类,会调用WebappClassLoader的findClass方法,如下:
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug(" findClass(" + name + ")");
// Cannot load anything from local repositories if class loader is stopped
if (!started) {
throw new ClassNotFoundException(name);
}
// (1) Permission to define this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled())
log.trace(" securityManager.checkPackageDefinition");
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled())
log.trace(" -->Exception-->ClassNotFoundException", se);
throw new ClassNotFoundException(name, se);
}
}
}
// Ask our superclass to locate this class, if possible
// (throws ClassNotFoundException if it is not found)
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
if (hasExternalRepositories && searchExternalFirst) {
try {
clazz = super.findClass(name);
} catch(ClassNotFoundException cnfe) {
// Ignore - will search internal repositories next
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if ((clazz == null)) {
try {
clazz = findClassInternal(name);
} catch(ClassNotFoundException cnfe) {
if (!hasExternalRepositories || searchExternalFirst) {
throw cnfe;
}
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if ((clazz == null) && hasExternalRepositories && !searchExternalFirst) {
try {
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn("WebappClassLoader.findClassInternal(" + name
+ ") security exception: " + ace.getMessage(), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
if (clazz == null) {
if (log.isDebugEnabled())
log.debug(" --> Returning ClassNotFoundException");
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled())
log.trace(" --> Passing on ClassNotFoundException");
throw e;
}
// Return the class we have located
if (log.isTraceEnabled())
log.debug(" Returning class " + clazz);
if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return (clazz);
}
不考虑使用SecurityManager的情况,最终会调用findClassInternal方法,如下:
protected Class<?> findClassInternal(String name)
throws ClassNotFoundException {
if (!validate(name))
throw new ClassNotFoundException(name);
String tempPath = name.replace('.', '/');
String classPath = tempPath + ".class";
ResourceEntry entry = null;
if (securityManager != null) {
PrivilegedAction<ResourceEntry> dp =
new PrivilegedFindResourceByName(name, classPath);
entry = AccessController.doPrivileged(dp);
} else {
entry = findResourceInternal(name, classPath);
}
if (entry == null)
throw new ClassNotFoundException(name);
Class<?> clazz = entry.loadedClass;
if (clazz != null)
return clazz;
synchronized (this) {
clazz = entry.loadedClass;
if (clazz != null)
return clazz;
if (entry.binaryContent == null)
throw new ClassNotFoundException(name);
// Looking up the package
String packageName = null;
int pos = name.lastIndexOf('.');
if (pos != -1)
packageName = name.substring(0, pos);
Package pkg = null;
if (packageName != null) {
pkg = getPackage(packageName);
// Define the package (if null)
if (pkg == null) {
try {
if (entry.manifest == null) {
definePackage(packageName, null, null, null, null,
null, null, null);
} else {
definePackage(packageName, entry.manifest,
entry.codeBase);
}
} catch (IllegalArgumentException e) {
// Ignore: normal error due to dual definition of package
}
pkg = getPackage(packageName);
}
}
if (securityManager != null) {
// Checking sealing
if (pkg != null) {
boolean sealCheck = true;
if (pkg.isSealed()) {
sealCheck = pkg.isSealed(entry.codeBase);
} else {
sealCheck = (entry.manifest == null)
|| !isPackageSealed(packageName, entry.manifest);
}
if (!sealCheck)
throw new SecurityException
("Sealing violation loading " + name + " : Package "
+ packageName + " is sealed.");
}
}
try {
clazz = defineClass(name, entry.binaryContent, 0,
entry.binaryContent.length,
new CodeSource(entry.codeBase, entry.certificates));
} catch (UnsupportedClassVersionError ucve) {
throw new UnsupportedClassVersionError(
ucve.getLocalizedMessage() + " " +
sm.getString("webappClassLoader.wrongVersion",
name));
}
/**
* 加载完某个类后把该类的二进制字节数组等信息置为null,帮助gc,只保留class
*/
entry.loadedClass = clazz;
entry.binaryContent = null;
entry.source = null;
entry.codeBase = null;
entry.manifest = null;
entry.certificates = null;
}
return clazz;
}
这里主要包含2步:
step4.1:entry = findResourceInternal(name, classPath);
这一步在WEB-INF/classes/和WEB-INF/lib/*.jar中根据类的name搜索,找到后封装成ResourceEntry返回。
protected ResourceEntry findResourceInternal(String name, String path) {
if (!started) {
log.info(sm.getString("webappClassLoader.stopped", name));
return null;
}
if ((name == null) || (path == null))
return null;
ResourceEntry entry = resourceEntries.get(name);
if (entry != null)
return entry;
int contentLength = -1;
InputStream binaryStream = null;
boolean isClassResource = path.endsWith(".class");
int jarFilesLength = jarFiles.length;
int repositoriesLength = repositories.length;
int i;
Resource resource = null;
boolean fileNeedConvert = false;
for (i = 0; (entry == null) && (i < repositoriesLength); i++) {
try {
String fullPath = repositories[i] + path;
Object lookupResult = resources.lookup(fullPath);
if (lookupResult instanceof Resource) {
resource = (Resource) lookupResult;
}
// Note : Not getting an exception here means the resource was
// found
ResourceAttributes attributes =
(ResourceAttributes) resources.getAttributes(fullPath);
contentLength = (int) attributes.getContentLength();
String canonicalPath = attributes.getCanonicalPath();
if (canonicalPath != null) {
// we create the ResourceEntry based on the information returned
// by the DirContext rather than just using the path to the
// repository. This allows to have smart DirContext implementations
// that "virtualize" the docbase (e.g. Eclipse WTP)
entry = findResourceInternal(new File(canonicalPath), "");
} else {
// probably a resource not in the filesystem (e.g. in a
// packaged war)
entry = findResourceInternal(files[i], path);
}
entry.lastModified = attributes.getLastModified();
if (resource != null) {
try {
binaryStream = resource.streamContent();
} catch (IOException e) {
return null;
}
if (needConvert) {
if (path.endsWith(".properties")) {
fileNeedConvert = true;
}
}
// Register the full path for modification checking
// Note: Only syncing on a 'constant' object is needed
synchronized (allPermission) {
int j;
long[] result2 =
new long[lastModifiedDates.length + 1];
for (j = 0; j < lastModifiedDates.length; j++) {
result2[j] = lastModifiedDates[j];
}
result2[lastModifiedDates.length] = entry.lastModified;
lastModifiedDates = result2;
String[] result = new String[paths.length + 1];
for (j = 0; j < paths.length; j++) {
result[j] = paths[j];
}
result[paths.length] = fullPath;
paths = result;
}
}
} catch (NamingException e) {
// Ignore
}
}
if ((entry == null) && (notFoundResources.containsKey(name)))
return null;
JarEntry jarEntry = null;
synchronized (jarFiles) {
try {
if (!openJARs()) {
return null;
}
for (i = 0; (entry == null) && (i < jarFilesLength); i++) {
jarEntry = jarFiles[i].getJarEntry(path);
if (jarEntry != null) {
entry = new ResourceEntry();
try {
entry.codeBase = getURI(jarRealFiles[i]);
String jarFakeUrl = entry.codeBase.toString();
jarFakeUrl = "jar:" + jarFakeUrl + "!/" + path;
entry.source = new URL(jarFakeUrl);
entry.lastModified = jarRealFiles[i].lastModified();
} catch (MalformedURLException e) {
return null;
}
contentLength = (int) jarEntry.getSize();
try {
entry.manifest = jarFiles[i].getManifest();
binaryStream = jarFiles[i].getInputStream(jarEntry);
} catch (IOException e) {
return null;
}
// Extract resources contained in JAR to the workdir
if (antiJARLocking && !(path.endsWith(".class"))) {
byte[] buf = new byte[1024];
File resourceFile = new File
(loaderDir, jarEntry.getName());
if (!resourceFile.exists()) {
Enumeration<JarEntry> entries =
jarFiles[i].entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry2 = entries.nextElement();
if (!(jarEntry2.isDirectory())
&& (!jarEntry2.getName().endsWith
(".class"))) {
resourceFile = new File
(loaderDir, jarEntry2.getName());
try {
if (!resourceFile.getCanonicalPath().startsWith(
canonicalLoaderDir)) {
throw new IllegalArgumentException(
sm.getString("webappClassLoader.illegalJarPath",
jarEntry2.getName()));
}
} catch (IOException ioe) {
throw new IllegalArgumentException(
sm.getString("webappClassLoader.validationErrorJarPath",
jarEntry2.getName()), ioe);
}
File parentFile = resourceFile.getParentFile();
if (!parentFile.mkdirs() && !parentFile.exists()) {
// Ignore the error (like the IOExceptions below)
}
FileOutputStream os = null;
InputStream is = null;
try {
is = jarFiles[i].getInputStream
(jarEntry2);
os = new FileOutputStream
(resourceFile);
while (true) {
int n = is.read(buf);
if (n <= 0) {
break;
}
os.write(buf, 0, n);
}
resourceFile.setLastModified(
jarEntry2.getTime());
} catch (IOException e) {
// Ignore
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
// Ignore
}
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
// Ignore
}
}
}
}
}
}
}
}
if (entry == null) {
synchronized (notFoundResources) {
notFoundResources.put(name, name);
}
return null;
}
/* Only cache the binary content if there is some content
* available and either:
* a) It is a class file since the binary content is only cached
* until the class has been loaded
* or
* b) The file needs conversion to address encoding issues (see
* below)
*
* In all other cases do not cache the content to prevent
* excessive memory usage if large resources are present (see
* https://issues.apache.org/bugzilla/show_bug.cgi?id=53081).
*/
if (binaryStream != null &&
(isClassResource || fileNeedConvert)) {
byte[] binaryContent = new byte[contentLength];
int pos = 0;
try {
while (true) {
int n = binaryStream.read(binaryContent, pos,
binaryContent.length - pos);
if (n <= 0)
break;
pos += n;
}
} catch (IOException e) {
log.error(sm.getString("webappClassLoader.readError", name), e);
return null;
}
if (fileNeedConvert) {
// Workaround for certain files on platforms that use
// EBCDIC encoding, when they are read through FileInputStream.
// See commit message of rev.303915 for details
// http://svn.apache.org/viewvc?view=revision&revision=303915
String str = new String(binaryContent,0,pos);
try {
binaryContent = str.getBytes(CHARSET_UTF8);
} catch (Exception e) {
return null;
}
}
/**
* 类的二进制字节数组
*/
entry.binaryContent = binaryContent;
// The certificates are only available after the JarEntry
// associated input stream has been fully read
if (jarEntry != null) {
entry.certificates = jarEntry.getCertificates();
}
}
} finally {
if (binaryStream != null) {
try {
binaryStream.close();
} catch (IOException e) { /* Ignore */}
}
}
}
// Add the entry in the local resource repository
synchronized (resourceEntries) {
// Ensures that all the threads which may be in a race to load
// a particular class all end up with the same ResourceEntry
// instance
ResourceEntry entry2 = resourceEntries.get(name);
if (entry2 == null) {
/**
* 对类进行缓存,下次用到直接返回
*/
resourceEntries.put(name, entry);
} else {
entry = entry2;
}
}
return entry;
}
step4.2:clazz = defineClass(name, entry.binaryContent, 0,
entry.binaryContent.length,
new CodeSource(entry.codeBase, entry.certificates));
4.1中找到了该类并且拿到了该类的字节数组,step4.2调用ClassLoader的defineClass方法,将该类的字节数组转换为jvm能够识别的运行时class对象。
通过上述源码分析,tomcat类加载的规则如下:
(1)首先检查WebappClassLoader加载的类缓存中有没有加载过该类,如果有则返回,否则进入(2);
(2)从jvm的系统类加载器的类缓存中检查有没有加载过该类,如果有则返回,否则进入(3);
(3)通过系统类加载器加载该类,如果能加载到则返回,负责进入(4);
(4)在/WEB-INF/classes/和/WEB-INF/lib/*.jar中寻找该类,如果寻找到该类,把该类对应的class字节码文件转换为字节流,然后调用ClassLoader的defineClass方法将该类字节码文件的字节流转换为运行期class类对象,完成类的加载,否则抛出ClassNotFoundException。
附:WebappClassLoader类的成员:
成员 | 类型 | 说明 |
---|---|---|
resources | DirContext | 该应用的资源对象 |
resourceEntries | HashMap<String, ResourceEntry> | 该应用所有缓存的类及对应的ResourceEntry |
notFoundResources | LinkedHashMap<String, String> | 该应用所有找不到的类缓存,当下次加载某个类时如果在notFoundResources中找不到,直接抛出ClassNotFoundException。 |
delegate | boolean | 在从该应用的仓库加载类之前是否首先代理给父加载器加载,默认为false,即优先从本应用的类仓库加载该类 |
lastJarAccessed | long | Last time a JAR was accessed. |
repositories | String[] | 该应用的类仓库,一般只有/WEB-INF/classes/ |
repositoryURLs | URL[] | 该应用所有类所在目录的URL |
files | File[] | 应用自身的类所在文件目录,不包含依赖的jar,一般为/WEB-INF/classes |
jarRealFiles | File[] | 应用依赖的所有jar包的路径 |
jarPath | String | jar文件监控路径,一般为/WEB-INF/lib |
jarNames | String[] | jar文件名列表 |
lastModifiedDates | long[] | 这个数组的大小与paths一致,对应paths中每个jar或class的更新时间戳 |
paths | String[] | 所有类资源的路径,包含/WEB-INF/lib/下的所有jar和/WEB-INF/classes/下的所有class |
loaderDir | File | Path where resources loaded from JARs will be extracted. |
canonicalLoaderDir | String | Path where resources loaded from JARs will be extracted. |
loaderPC | HashMap<String, PermissionCollection> | The PermissionCollection for each CodeSource for a web application context. |
parent | ClassLoader | 父加载器。 |
system | ClassLoader | 系统类加载器,即tomcat的应用类加载器,类加载路径为$CATALINA_HOME/bin/下的类 |
started | boolean | 组件是否已经启动。 |
contextName | String | Name of associated context used with logging and JMX to associate with the right web application. Particularly useful for the clear references messages. Defaults to unknown but if standard Tomcat components are used it will be updated during initialisation from the resources. |
resources成员的变量如下:
resources.png
resourceEntries成员如下,它是1个k->v结构,key是类的相对WEB-INF/classes/或WEB-INF/lib/.jar的相对路径文件名,value是ResourceRntry,这个成员维持着该应用已经加载了的类的缓存,下次加载如果能从该缓存中找到该类则直接用缓存的类,同样,在热部署机制中,当扫描到/WEB-INF/classes/和/WEB-INF/lib/.jar有文件的时间戳更新,了则会停止WebappClassLoader并重新启动WebappClassLoader,停止的过程中会清空resourceEntries。
repositories成员如下,这个数组一般只包含1个成员"/WEB-INF/classes/"。
repositories.png
repositoryURLs成员是该应用的类路径,包括/WEB-INF/classes/和/WEB-INF/lib/下的所有jar包。
repositoryURLs.png
files是应用自身的类路径,如下:
files.png
jarRealFiles是该应用依赖的所有jar包的绝对路径,如下:
jarRealFiles.png
jarPath如下:
jarPath.png
jarNames如下:
jarNames.png
paths如下,包含/WEB-INF/classes/下的类路径和WEB-INF/lib/下的jar路径:
paths.png
loaderDir和canonicalLoaderDir如下:
loaderDir.png canonicalLoaderDir.png
contextName如下:
contextName.png