Tomcat8源码第一探
本人非大神,希望站在普通水平角度与大家共勉。语言尽量通俗,文中说明不当的,欢迎指正!
Tomcat和Servlet
我们在最开始接触Java Web的时候都接触过Servlet,应用交互的时候,为了不要Java程序员去封装复杂的request和response,所以以Servlet作为规范。同样的,tomcat端可以读取Servlet的代码,像以前就是在web.xml里面配置一样。
tomcat端会加载很多Servlet,所以也称之为Servlet容器。而配过Tomcat的都知道server.xml,我们曾在server.xml里有过如下的类似配置
在Tomcat源码中,你会发现在server.xml中很多xml标签都能转换成对应的类或属性,而我们以前靠Context就可以启动一个web应用的话,那么我们先从这个最熟悉的部分开始,而且Servlet的处理肯定也是和这个相关。
Tomcat源码怎么导入,可以参考他人博客《tomcat8 源码 导入eclipse》,IDEA同理!
有趣的推测
话接上文,先看下Context,我们可以看到源码中在包org.apache.catalina下有Context这个接口
如果是平常看过一些源码或者平时对自己命名规范比较有要求的小伙伴,都知道接口的实现类中一般都会有个前缀带上Standard的标准实现。果不其然,我们同样可以找到StandardContext,如下图:
那怎么看这个类呢?再进一步靠感觉推测,我们知道一个类的初始方法通常会叫init或者load之类的,我们搜下方法,可以发现有个loadOnStartup的方法,如下图:
至此,我们大概对看Tomcat源码有感觉了,可以从我们熟悉的部分找到源码中对应的接口,再找其标准的实现,类找到后我们可以对应找其切入方法。
浅窥Tomcat8源码
既然Tomcat是个java项目,那它必然是有一个启动类和主方法入口。我完全凭感觉搜索public static void main的时候,我们会发现有个BootStrap(翻译:引导程序)类,如下图:
那我们大致可以知道,Tomcat的启动入口就在这个BootStrap.mian()中。
可以先看下这个main方法,
下面为了助于阅读,我直接把部分说明用中文注释写到代码中,可以跟着我的中文注释简要理解下源码
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
// 初始化的一些操作
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to prevent
// a range of class not found exceptions.
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); // 把server.xml文件中标签的内容解析出来,可以创建对应的实体类
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 t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
然后我们可以详细看下上面代码中有注释的daemon.load(args)方法,
/**
* Load daemon.
*/
private void load(String[] arguments)
throws Exception {
// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled())
log.debug("Calling startup class " + method);
method.invoke(catalinaDaemon, param);
}
你会发现这段代码用到了反射的原理,调用了一个类为catalinaDaemon,methodName = "load"的无参方法。可以不用深究,根据命名,就知道大概会是一个叫Catalina类中会有load方法。于是我们找寻Catalina类。
趣味补充
为什么这里要叫Catalina?
Tomcat的这个单词的意思是“公猫”,因为它的开发者姆斯·邓肯·戴维森希望用一种能够自己照顾自己的动物代表这个软件,于是命名为tomcat,它的Logo兼吉祥物也被设计成了一只公猫形象。
Catalina是美国西海岸靠近洛杉矶22英里的一个小岛,因为其风景秀丽而著名。Servlet运行模块的最早开发者Craig McClanahan因为喜欢Catalina岛故以Catalina命名他所开这个模块,尽管他从来也没有去过那里。
另外在开发的早期阶段,Tomcat是被搭建在一个叫Avalon的服务器框架上,而Avalon则是Catalina岛上的一个小镇的名字,于是想一个与小镇名字相关联的单词也是自然而然。还有一个原因来自于Craig McClanahan养的猫,他养的猫在他写程序的时候喜欢在电脑周围闲逛。
可能是Catalina岛是个悠闲散步的好地方,猫的闲逛让Craig McClanahan想起了那里。
所以Catalina内部寓意就是tomcat的脚本文件,寄寓,是个小岛的名字,开发者曾在岛上生活过。
根据上面我们提到的Catalina类中的load方法,我们来找下这个方法,先不具体看代码,你会发现该方法头上有注释如下
/**
* Start a new server instance.
*/
public void load() {
这段英文意思就是“开启一个新的服务实例”,到此,你会想到什么?
对了,这不就对应前面说的Tomcat配置Server.xml中最外层的Server标签吗?
接下来,再随着我的注释看下里面的代码
/**
* Start a new server instance.
*/
public void load() {
if (loaded) {
return;
}
loaded = true;
// 获取开始时间
long t1 = System.nanoTime();
// 目录初始化
initDirs();
// Before digester - it may be needed
// 命名规则初始化
initNaming();
// Create and execute our Digester
// 创建一个解析器用以解析Server.xml文件的
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
// 获取文件
file = configFile();
// 用流去读取文件
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
}
// This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
/* Maven也有Tomcat插件,SpringBoot有内置的Tomcat插件,区别就是读取的配置文件不一样,从下面代码可以看到,内置的Tomcat一般读取的是server-embed.xml配置文件,embed也是嵌入、内置的意思 */
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
}
if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
}
try {
inputSource.setByteStream(inputStream);
digester.push(this);
// 上面是一些检查,至此终于开始解析了
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
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("Catalina.start", e);
}
}
// 获取结束时间
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
// 打印一共花了多少时间
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}
上面中文注释提到的都是我们很熟悉的Tomcat的部分。
继续上面的内容,看下getServer().init();,这里开始了Server的一个初始化操作,进入init方法看下。然后就到了Lifecycle的接口中,我们会发现Lifecycle也是有一个init方法。而通常一个接口在架构中都有一个默认实现类,而这个默认实现类就是……看下图,看名字,你大概也会有感觉。
对,就是上面这个LifecycleBase,默认实现类。那就看下这个LifecycleBase.init()方法
@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) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(
sm.getString("lifecycleBase.initFail",toString()), t);
}
}
源码的命名都是很讲究的,我们会发现这个initInternal()就是这方法的关键。
然而这是个抽象方法,也是要被子类实现的。且它有很多个子类去实现,如下图:
我们并不知道是哪个,但是可以确定的是我们是要取初始化Server,于是我们搜索Server,自然而然得到了StandardServer。
下面,我引入一张网上找的Tomcat架构组成图,来助于后面的解读
我们知道,Server初始化完了之后肯定也需要初始化它内部的组成部分,根据上面的架构图也可以分析下StandardServer.initInternal()源码(Internal即:内部的)
/**
* Invoke a pre-startup initialization. This is used to allow connectors
* to bind to restricted ports under Unix operating environments.
*/
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// Register global String cache
// Note although the cache is global, if there are multiple Servers
// present in the JVM (may happen when embedding) then the same cache
// will be registered under multiple names
onameStringCache = register(new StringCache(), "type=StringCache");
// Register the MBeanFactory
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");
// Register the naming resources
// Global Naming的初始化
globalNamingResources.init();
// 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进行检查
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();
}
}
// Initialize our defined Services
/** 重点来了,Service的初始化,这里为什么用到for循环,因为我们会注册多个服务,
架构图中也有多层的service,说明service是有多个的,也说明Service标签可以配置多个。*/
for (int i = 0; i < services.length; i++) {
services[i].init();
}
}
我们从上面代码最后的services[i].init()往下走,又回到了Lifecycle,显然,它又没有实现。
又交给其子类LifecycleBase去实现。有没有发现我们又回到了LifecycleBase?那么和找StandardServer同理,我们找下StandardService。结合架构图看其init方法,你会发现Service下面的这些组成部分也开始初始化了。
/**
* Invoke a pre-startup initialization. This is used to allow connectors
* to bind to restricted ports under Unix operating environments.
*/
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// 根据架构图,引擎初始化
if (engine != null) {
engine.init();
}
// Initialize any Executors
// 执行器也是有多个的,执行器也循环初始化,架构图中是和Connector交互的
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// Initialize mapper listener
mapperListener.init();
// Initialize our defined Connectors
synchronized (connectorsLock) {
// Connector也用到了for循环,说明也可以用有多个Connector,可以配置多个
for (Connector connector : connectors) {
try {
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}
从connector.init往下走,你会发现它又回到了LifecycleBase.init的方法中,又回来了一次。(根据组成图)同理,接下来是Connector实例化,所以理所应当有个子类是和Connector相关的,我们发现是真的有个Connector,如下图:
Connector?觉不觉得这个单词很熟悉?对了!我们平时在Server.xml配置启动端口的时候不就是用的Connector标签吗?再找其initInternal()方法。
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
// Initialize adapter 协议适配
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
// 忽略其他代码
……
try {
// 协议操作的一个init方法
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}
根据上面的protocolHandler.init(),接下来到AbstractProtocol.init方法,会发现方法的最后一句还是有一个endpoint.init()
@Override
public void init() throws Exception {
// 忽略其他代码
……
endpoint.init();
}
再进init方法,接下来到AbstractEndpoint.init方法。
public void init() throws Exception {
if (bindOnInit) {
bind();
bindState = BindState.BOUND_ON_INIT;
}
// 忽略其他代码
……
}
发现代码中有个bind方法,bind是什么意思?绑定的意思啊,不就是我们平常说的端口绑定吗!
总结
到此,本文篇幅有限,只是提供个看Tomcat源码的思路和方法。前面说的这么多,总结起来就一张图:
结合源码和上图也可以发现,Lifecycle在这篇讲的部分中到protocolHandler.init(),就不再交给Lifecycle了。
参考
《tomcat8 源码 导入eclipse》
百度百科——Catalina
百度百科——tomcat