由于在生产环境中,Tomcat一般部署在Linux系统下,所以本文将以 startup.sh shell脚本为准,对Tomcat的启动进行分析。
我们启动Tomcat的命令如下:
sh startup.sh
startup.sh的脚本代码如下:
# Better OS/400 detection: see Bugzilla 31132
os400=false
case "`uname`" in
OS400*) os400=true;;
esac
# resolve links - $0 may be a softlink
PRG="$0"
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`/"$link"
fi
done
PRGDIR=`dirname "$PRG"`
EXECUTABLE=catalina.sh
# Check that target executable exists
if $os400; then
# -x will Only work on the os400 if the files are:
# 1. owned by the user
# 2. owned by the PRIMARY group of the user
# this will not work if the user belongs in secondary groups
eval
else
if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
echo "Cannot find $PRGDIR/$EXECUTABLE"
echo "The file is absent or does not have execute permission"
echo "This file is needed to run this program"
exit 1
fi
fi
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
重点看最后一行代码:exec "EXECUTABLE" start "$@"
这里有两个变量:
- PRGDIR:当前shell脚本所在的路径;
- EXECUTABLE:脚本catalina.sh。
通过以上变量的解释,我们知道了其实执行startup.sh就是在执行catalina.sh,并且传递了参数start。
catalina.sh中接收到start参数后的执行的脚本分支见代码如下:
elif [ "$1" = "start" ] ; then
# 此处省略参数校验的脚本
shift
touch "$CATALINA_OUT"
if [ "$1" = "-security" ] ; then
if [ $have_tty -eq 1 ]; then
echo "Using Security Manager"
fi
shift
eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
-Djava.security.manager \
-Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start \
>> "$CATALINA_OUT" 2>&1 "&"
else
eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-Djava.endorsed.dirs="\"$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 "&"
fi
if [ ! -z "$CATALINA_PID" ]; then
echo $! > "$CATALINA_PID"
fi
echo "Tomcat started."
从以上代码片段可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数也是start。Bootstrap的main方法的实现见代码如下:
public static void main(String[] args) {
synchronized(daemonLock) {
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable var5) {
handleThrowable(var5);
var5.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable var7) {
Throwable t = var7;
if (var7 instanceof InvocationTargetException && var7.getCause() != null) {
t = var7.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
可以很清晰的看到,一上来就调用了 bootstrap.init();方法进行初始化的操作,代码如下:
public void init() throws Exception {
this.initClassLoaders();
Thread.currentThread().setContextClassLoader(this.catalinaLoader);
SecurityClassLoad.securityClassLoad(this.catalinaLoader);
if (log.isDebugEnabled()) {
log.debug("Loading startup class");
}
Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
if (log.isDebugEnabled()) {
log.debug("Setting startup class properties");
}
String methodName = "setParentClassLoader";
Class<?>[] paramTypes = new Class[]{Class.forName("java.lang.ClassLoader")};
Object[] paramValues = new Object[]{this.sharedLoader};
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
this.catalinaDaemon = startupInstance;
}
而init方法一上来调用了这三行关键代码:
- this.initClassLoaders();
-
Thread.currentThread().setContextClassLoader(this.catalinaLoader);
catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身容器下的class
-
SecurityClassLoad.securityClassLoad(this.catalinaLoader);
securityClassLoad方法主要加载Tomcat容器所需的class,包括:
- Tomcat核心class,即org.apache.catalina.core路径下的class;
- org.apache.catalina.loader.WebappClassLoader$PrivilegedFindResourceByName;
- Tomcat有关session的class,即org.apache.catalina.session路径下的class;
- Tomcat工具类的class,即org.apache.catalina.util路径下的class;
- javax.servlet.http.Cookie;
- Tomcat处理请求的class,即org.apache.catalina.connector路径下的class;
- Tomcat其它工具类的class,也是org.apache.catalina.util路径下的class;
ok,Common/Catalina/Shared ClassLoader 已经创建好了,那么肯定是要被使用的,是在哪里使用的呢?它们之间同Webapp ClassLoader又是怎么联系起来的?我们继续看init方法:
public void init() throws Exception {
this.initClassLoaders();
Thread.currentThread().setContextClassLoader(this.catalinaLoader);
SecurityClassLoad.securityClassLoad(this.catalinaLoader);
if (log.isDebugEnabled()) {
log.debug("Loading startup class");
}
//1.先加载Catalina类
Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
//2.通过反射实例化对象
Object startupInstance = startupClass.getConstructor().newInstance();
if (log.isDebugEnabled()) {
log.debug("Setting startup class properties");
}
//3.catalina的setParentClassLoader方法
String methodName = "setParentClassLoader";
Class<?>[] paramTypes = new Class[]{Class.forName("java.lang.ClassLoader")};
Object[] paramValues = new Object[]{this.sharedLoader};\
//4.将catalina设置为sharedLoader的上级加载器
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
this.catalinaDaemon = startupInstance;
}
在我们上面的securityClassLoad()方法中会执行 loadCorePackage方法,该方法源码如下:
private static final void loadCorePackage(ClassLoader loader) throws Exception {
String basePackage = "org.apache.catalina.core.";
loader.loadClass("org.apache.catalina.core.AccessLogAdapter");
loader.loadClass("org.apache.catalina.core.ApplicationContextFacade$PrivilegedExecuteMethod");
loader.loadClass("org.apache.catalina.core.ApplicationDispatcher$PrivilegedForward");
loader.loadClass("org.apache.catalina.core.ApplicationDispatcher$PrivilegedInclude");
loader.loadClass("org.apache.catalina.core.ApplicationPushBuilder");
loader.loadClass("org.apache.catalina.core.AsyncContextImpl");
loader.loadClass("org.apache.catalina.core.AsyncContextImpl$AsyncRunnable");
loader.loadClass("org.apache.catalina.core.AsyncContextImpl$DebugException");
loader.loadClass("org.apache.catalina.core.AsyncListenerWrapper");
loader.loadClass("org.apache.catalina.core.ContainerBase$PrivilegedAddChild");
loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$AnnotationCacheEntry");
loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$AnnotationCacheEntryType");
loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedGetField");
loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedGetMethod");
loader.loadClass("org.apache.catalina.core.DefaultInstanceManager$PrivilegedLoadClass");
loader.loadClass("org.apache.catalina.core.ApplicationHttpRequest$AttributeNamesEnumerator");
}
其中 loader.loadClass("org.apache.catalina.core.ContainerBase$PrivilegedAddChild"); 会加载到 ContainerBase以及其子类,通过查看发现其子类有如下:
image而StandardContext类就是一个核心的子类,因为在其 startInternal()方法中:
/**
* Start this component and implement the requirements
* of {@link LifecycleBase#startInternal()}.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
@Override
protected synchronized void startInternal() throws LifecycleException {
// 省略前边的代码
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// 省略中间的代码
// Start our subordinate components, if any
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
// 省略后边的代码
}
我们发现了关键的 WebappLoader 的创建,并且将WebappLoader设置为了当前的类加载器。
同时创建WebappLoader 的时候,传递了一个参数 getParentClassLoader() ,我们可以看下 WebappLoader的构造函数:
public WebappLoader(ClassLoader parent) {
this.classLoader = null;
this.context = null;
this.delegate = false;
this.loaderClass = ParallelWebappClassLoader.class.getName();
this.parentClassLoader = null;
this.reloadable = false;
this.support = new PropertyChangeSupport(this);
this.classpath = null;
this.parentClassLoader = parent;
}
ok,最后一行代码给webappLoader设置了对应的父级类加载器,getParentClassLoader() 这个方法会获取父容器parentClassLoader的属性,也就是找到ContainerBase中的setParentClassLoader方法被谁调用了就知道附的啥值了:
image可以看到这个设置方法被CopyParentClassLoaderRule的begin方法中赋值了:
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
if (digester.getLogger().isDebugEnabled())
digester.getLogger().debug("Copying parent class loader");
Container child = (Container) digester.peek(0);
Object parent = digester.peek(1);
Method method =
parent.getClass().getMethod("getParentClassLoader", new Class[0]);
ClassLoader classLoader =
(ClassLoader) method.invoke(parent, new Object[0]);
child.setParentClassLoader(classLoader);
}
这里的classLoader其实就是通过反射取出来的 SharedLoader,这里刚好就与我们的BootStrap中init刚开始的赋值匹配上了:
image因此,我们也能彻底知道,WebAppLoader的父级类加载器就是我们的ShareClassLoader了。
代码阅读到这里,已经基本清楚了Tomcat中ClassLoader的总体结构,总结如下:
Tomcat存在common、catalina、shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,而针对每个webapp,也就是context(对应代码中的StandardContext类),都有自己的WebappClassLoader实例来加载每个应用自己的类,该类加载实例的parent即是Shared ClassLoader。
OK,在理解了整个类加载器结构后,我们再来看 WebAppLoader 是如何加载我们的类的,重点查看 loadClass 方法,由于代码过长,直接截取核心代码:
①从本地缓存中查找:
image protected Class<?> findLoadedClass0(String name) {
String path = this.binaryNameToPath(name, true);
ResourceEntry entry = (ResourceEntry)this.resourceEntries.get(path);
return entry != null ? entry.loadedClass : null;
}
②缓存中没有,则从JVM的引导类Bootstrap类加载器加载:
image该类加载器包含 Java 虚拟机提供的基本运行时类,以及系统扩展目录 ( $JAVA_HOME/jre/lib/ext
) 中存在的 JAR 文件中的任何类。
③再从系统类加载器进行查找:
imageSystem这个类加载器通常从CLASSPATH
环境变量的内容中初始化。所有这些类对 Tomcat 内部类和 Web 应用程序都是可见的。但是,标准的 Tomcat 启动脚本($CATALINA_HOME/bin/catalina.sh
或 %CATALINA_HOME%\bin\catalina.bat
)完全忽略了CLASSPATH
环境变量本身的内容,而是从以下存储库构建 System 类加载器:
-
$CATALINA_HOME/bin/bootstrap.jar — 包含用于初始化 Tomcat 服务器的 main() 方法,以及它所依赖的类加载器实现类。
-
CATALINA_HOME/bin/tomcat-juli.jar — 日志实现类。其中包括
java.util.logging
API 的增强类 ,称为 Tomcat JULI,以及 Tomcat 内部使用的 Apache Commons Logging 库的包重命名副本。有关更多详细信息,请参阅日志记录文档。如果
tomcat-juli.jar
存在于 CATALINA_HOME/bin 中的那个 。它在某些日志记录配置中很有用 -
$CATALINA_HOME/bin/commons-daemon.jar — 来自Apache Commons Daemon项目的类。这个 JAR 文件不存在于
CLASSPATH
构建者catalina.bat
|.sh
脚本,但从bootstrap.jar的清单文件中引用。
④由当前类加载器进行加载:
image⑤最后由父类加载器加载
image总结:
Web应用类加载器默认的加载顺序是:
(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器查找;
(3).如果没有,则从当前类加载器加载查找(按照WEB-INF/classes、WEB-INF/lib的顺序);
(4).如果没有,则从父类加载器加载,父类加载器采用默认的委派模式
tomcat提供了delegate属性用于控制是否启用java委派模式,默认false(不启用),当设置为true时,tomcat将使用java的默认委派模式,这时加载顺序如下:
(1).先从缓存中加载;
(2).如果没有,则从JVM的Bootstrap类加载器加载;
(3).如果没有,则从父类加载器加载,加载顺序是AppClassLoader、Common、Shared。
(4).如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序);
思考题:
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver
正确加载的,你知道是怎么做的吗?
这个问题大家可以下来自己尝试阅读源码得到解决。
网友评论