Classloader 是怎么工作的

背景

工作需要将多个 Playframework 的应用 在一个 java 进程中跑起来
Java 出身的我自然的想到 tomcat 可以运行多个 war,也就等同于多个 Playframework 在一个 Java 进程中跑起来

Tomcat 如何运行多个 War

首先通过 Tomcat 的 startup.sh 跳到 catalina.sh,

1
2
3
4
5
6
7
8
eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
-classpath "\"$CLASSPATH\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\""
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start \
>> "$CATALINA_OUT" 2>&1 "&"

进入 org.apache.catalina.startup.Bootstrap 这个 tomcat 的启动入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void init() throws Exception {

initClassLoaders();

Thread.currentThread().setContextClassLoader(catalinaLoader);

SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();

// Set the shared extensions class loader
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);

catalinaDaemon = startupInstance;
}

private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}

Bootstrap 中的 调用 Init 初始化了 catalinaLoader 和 sharedLoader 是 commonClassLoder 的子类

  • commonClassloader
    • catalinaLoader
    • sharedLoader

然后看 Bootstrap.start, 实际上是 org.apache.catalina.startup.Catalina.start()
start() 会先 load Server, 将 tomcat 一系列的复杂组件进行初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* 启动 tomcat 组件的初始化方法,
*/
public void load() {
initDirs();
initNaming();
File file = configFile();
Digester digester = createStartDigester();
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

// Stream redirection
initStreams();

// Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error(sm.getString("catalina.initError"), e);
}
}

}

protected Digester createStartDigester() {
Digester digester = new Digester();
digester.setValidating(false);
digester.setRulesValidation(true);
Map<Class<?>, List<String>> fakeAttributes = new HashMap<>();
List<String> objectAttrs = new ArrayList<>();
objectAttrs.add("className");
fakeAttributes.put(Object.class, objectAttrs);
// Ignore attribute added by Eclipse for its internal tracking
List<String> contextAttrs = new ArrayList<>();
contextAttrs.add("source");
fakeAttributes.put(StandardContext.class, contextAttrs);
digester.setFakeAttributes(fakeAttributes);

...
return digester;

}
/**
* Start a new server instance.
*/
public void start() {

if (getServer() == null) {
load();
}
...
// Start the new server
getServer().start();
...
if (await) {
await();
stop();
}
}

public void load() {
getServer().init();
}

getServer().init() 的逻辑 在 Server 的子类 StandardServer 中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public abstract class LifecycleBase implements Lifecycle {
@Override
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}

try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}
}

// LifecycleMBeanBase extends LifecycleBase
public final class StandardServer extends LifecycleMBeanBase implements Server {
@Override
protected void initInternal() throws LifecycleException {
// Populate the extension validator with JARs from common and shared
// class loaders
if (getCatalina() != null) {
ClassLoader cl = getCatalina().getParentClassLoader();
// Walk the class loader hierarchy. Stop at the system class loader.
// This will add the shared (if present) and common class loaders
while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
for (URL url : urls) {
if (url.getProtocol().equals("file")) {
try {
File f = new File (url.toURI());
if (f.isFile() && f.getName().endsWith(".jar")) {
ExtensionValidator.addSystemResource(f);
}
} catch (URISyntaxException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
}
}
}
cl = cl.getParent();
}
}
}
}

StandardServer.init 逻辑实际是 继承自 LifecycleBase 并调用 自己的 initInternal
StandardServer.getCatalina().getParentClassLoader(); 实际上获取的就是 在 Catalina.load 过程中 设置的 Catalina 实例的属性 parentClassLoader
注释上写的很好,就是用 Catalina 里的 commonClassLoader 和 sharedClassLoader 去加载资源

接着看 StandardContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//class ContainerBase extends LifecycleMBeanBase
public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
protected synchronized void startInternal() throws LifecycleException {
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}

// Binding thread
ClassLoader oldCCL = bindThread();

try {
if (ok) {
// Start our subordinate components, if any
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
// By calling unbindThread and bindThread in a row, we setup the
// current Thread CCL to be the webapp classloader
unbindThread(oldCCL);
oldCCL = bindThread();
}
} finally {
// Unbinding thread
unbindThread(oldCCL);
}
}

protected ClassLoader bindThread() {

ClassLoader oldContextClassLoader = bind(false, null);

if (isUseNaming()) {
try {
ContextBindings.bindThread(this, getNamingToken());
} catch (NamingException e) {
// Silent catch, as this is a normal case during the early
// startup stages
}
}

return oldContextClassLoader;
}

public ClassLoader bind(boolean usePrivilegedAction, ClassLoader originalClassLoader) {
Loader loader = getLoader();
ClassLoader webApplicationClassLoader = null;
if (loader != null) {
webApplicationClassLoader = loader.getClassLoader();
}

if (originalClassLoader == null) {
if (usePrivilegedAction) {
PrivilegedAction<ClassLoader> pa = new PrivilegedGetTccl();
originalClassLoader = AccessController.doPrivileged(pa);
} else {
originalClassLoader = Thread.currentThread().getContextClassLoader();
}
}

if (webApplicationClassLoader == null ||
webApplicationClassLoader == originalClassLoader) {
// Not possible or not necessary to switch class loaders. Return
// null to indicate this.
return null;
}

ThreadBindingListener threadBindingListener = getThreadBindingListener();

if (usePrivilegedAction) {
PrivilegedAction<Void> pa = new PrivilegedSetTccl(webApplicationClassLoader);
AccessController.doPrivileged(pa);
} else {
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);
}
if (threadBindingListener != null) {
try {
threadBindingListener.bind();
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(
"standardContext.threadBindingListenerError", getName()), t);
}
}

return originalClassLoader;

}

核心逻辑就是 利用 Thread.currentThread().setContextClassLoader(webApplicationClassLoader) 来改变当前线程上下文中的 classloader 实例
通过 WebappClassLoader.start() 去加载 每个 war 里面的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void start() throws LifecycleException {

state = LifecycleState.STARTING_PREP;

WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
}

state = LifecycleState.STARTED;
}

总结

  • Classloader 可有效的将 class 资源进行隔离
  • Class.getClass.getClassLoader 获取的是加载 这个 class 的 classLoader
  • Thread.currentThread().getClassLoader() 获取的是当前线程上下文的 classloader
  • Thread.currentThread().setClassLoader(自定义classLoader) 可加载指定的 class 资源