再次强调一下,Tomcat系列全部文章基于9.0.12版本。
入门篇2 Tomcat的启动(一)
所有的java项目,程序启动入口其实都是main方法,tomcat也不例外,Tomcat的启动入口在Bootstrap.java中的main方法中。
那么,我们的启动流程就由此开始啦。debug模式,走起~
其实启动过程大体分为两步,第一部是init,第二步是start,各位看官先留一个印象,抓住主干。
一: init()方法的轮廓
1.首先在main方法中用了单例的懒汉模式来实例化对象,这里面daemon对象用了volatile关键字实现了双重检查锁,保证了线程安全。第一个关键点Bootstrap类中的 bootstrap.init(),那么tomcat究竟在初始化什么东西呢?
如果不存在,setContextClassLoader方法在做什么呢?这里先留一个悬念,大家往下看。
2.init方法的关键部分给各位观众老爷标识了出来。并且介绍了大致的作用。
public void init() throws Exception {
//初始化类加载器 commonLoader catalinaLoader sharedLoader
initClassLoaders();
//设置当前上下文的类加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);
//为catalinaLoader加载器设置安全保护,也就是限定可以加载哪些类
SecurityClassLoad.securityClassLoad(catalinaLoader);
if (log.isDebugEnabled())
log.debug("Loading startup class");
//根据catalinaLoader类加载器取得class对象
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
//根据catalinaLoader类获取实例对象
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
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);
//将当前的webapp类加载器的classLoader设置为sharedClassLoader
method.invoke(startupInstance, paramValues);
//传递使用catalinaLoader实例化的对象的引用
catalinaDaemon = startupInstance;
}
光看这些作用其实并没有什么用,想要真正弄懂,获得提升需要深入到具体的代码段。下面我们一步一步具体的来看大佬们是如何实现这些功能的。
二:深入底层
1..initClassLoaders()这个方法初始化了三个类加载器,为什么要初始化类加载器呢?第一,我们部署项目的时候,在一个项目里面,一个双亲委派模型基本实现了对jar包管理的需求,可是在一个tomcat下面可以放很多的war包,每个项目里面依赖的jar包互不干扰,然后tomcat还有自己依赖的jar包,这些jar包要被所有项目共享。第二,asp,php这些都实现了热部署,我们jsp当然不能示弱,这个时候tomcat就必须实现自己的classLoader。下面放一张tocmat的类加载器实现图。
图片转自 https://blog.csdn.net/fuzhongmin05/article/details/57404890
Common ClassLoader可以加载tomcat自己的jar和所有web应用的jar。
Catalina ClassLoader可以加载tomcat自己的核心jar包。
Shared ClassLoader可以加载所有web应用共享的jar包。
Webapp ClassLoader可以加载每一个web应用自己的jar包。
原有的概执行模式没有变,也是双亲委派模型的执行方式,先去父ClassLoader去找,如果没找到,就从上往下逐一寻找。我们现在所写的web应用几乎都是基于spring的,依赖的jar包都非常多,如果每一个项目的spring的jar包都放置在各自的war包中,那么内存会爆炸式的增长,这里我们就可以同一项目的依赖版本,然后将通用的依赖全部放置在shared 或者common类加载器加载,会节约很大的内存。
其实我当时看到这里的时候,我想到了三个问题。不知道各位看官有没有想到,看源码的时候一定不要被绕晕了,看的同时要有自己的理解,如果是自己实现的话是什么样的,然后再看看大佬们是怎么实现的,只有这样才会真正的理解源码,充实提升自己。
Question 1:大佬们是如何自定义类加载器,然后初始化的。为什么自定义的类加载器就能加载指定位置的jar包呢?
Question 2:既然已经初始化完类加载器了,为什么还要在当前线程中加入catalinaLoader呢?
Question 3:为什么要用反射的方式执行Catalina的setParentClassLoader()方法呢,这里直接new一个Catalina对象不行吗,岂不是方便快捷?
下面针对这三个问题进行解答,这三个问题搞清楚了,init方法也差不多就清楚了。
Answer 1:我们继续跟进initClassLoaders方法一探究竟。
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
//如果没有创建成功,则用appClassLoader
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);
}
}
createClassLoader方法第一个参数是name,第二个参数是父加载器。还不是核心代码,那我们继续深入到createClassLoader 方法。
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List<Repository> repositories = new ArrayList<>();
String[] repositoryPaths = getPaths(value);
for (String repository : repositoryPaths) {
// Check for a JAR URL repository 检查当前的url是不是网络路径,如果是的话放到 repositories里面
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
我们可以去全文搜索catalina.loader这个,然后你会在catalina.properties这个文件里面看到这么一行
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
这就是我们得到的value的值。 然后是replace方法,这个方法比较大,比较多。不适合把代码直接复制进来。我就说一下核心流程,大家对照着看一下应该也懂的七七八八。首先new一个stringBuilder对象,然后建立两个索引,一个是pos_start,一个是pos_end,然后用indexOf方法截取"${"和"}"出现的位置,然后对value字符串进行截取每一个括号里面的内容。然后根据内容获取相应的replacement比如,
else if (Globals.CATALINA_HOME_PROP.equals(propName)) {
replacement = getCatalinaHome();
}
public static final String CATALINA_HOME_PROP = "catalina.home";
Tomcat有一个Global的全局变量的类,截取后的结果符合相应的条件,就执行getCatalinaHome方法,大家注意,这个方法比较有趣。这个方法返回的是文件的路径,也就是catalinaHomeFile的getPath方法。大家看一下这个代码
private static final File catalinaHomeFile;
//这个正则的作用是截取""之间的内容,并且中间没有逗号分割的部分。在getPaths方法中会使用这个。
private static final Pattern PATH_PATTERN = Pattern.compile("(\".*?\")|(([^,])*)");
static {
// Will always be non-null
String userDir = System.getProperty("user.dir");
// Home first
String home = System.getProperty(Globals.CATALINA_HOME_PROP);
File homeFile = null;
if (home != null) {
File f = new File(home);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
这里用静态代码块初始化这个这个文件,文件的路径是system.getProperty方法,这个方法大家平时用的少。大家安装tomcat的时候还记得要设置一个环境变量叫catalinaHome吗,这个方法就是用来获取这个环境变量的值的。这一下是不是解答了好多童鞋的疑惑,为什么要配置环境变量,哈哈哈。 此时我们得到的value其实是这么一大串东西
"E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib","E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib/*.jar","E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib","E:\java\sourceCode\apache-tomcat-9.0.12-src\catalina-home/lib/*.jar"
,用getPaths方法将这些路径分割Akira,接下来就是遍历每一个仓库路径,先判断这个仓库路径是不是网络路径,文件路径和文件路径的种类绑定。有可能是url,glob,jar,dir类型。然后用工厂模式ClassLoaderFactory.createClassLoader方法创建类加载器,这个方法内容比较多,但是逻辑其实不复杂,无非就是读取每个仓库下面对应的文件,如果repository是.jar结尾的,那么就加载该目录下所有。jar结尾的文件。这里面还涉及一些对文件名的转换,所以我们的jar包名称尽量用英语。最后创建的核心代码是在return里面
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
AccessController.doPrivileged方法是jdk自带的创建受保护的类加载器的方法,new UrlClassLoader是创建加载器的方法。到这里第一个问题就清晰啦~。嘿嘿嘿,不知道各位看官有没有跟上。应该比较通俗易懂了吧。
Answer 2: 第二个问题其实一个场景大家就明白了,比如我们如果将spring的jar包都放置在共享的目录中了,现在我们每一个项目需要配置数据源,spring启动的时候会去读取xml文件或者properties内容然后初始化连接池。这时候就违背了双亲委派机制。父级类加载器需要读取孩子加载器的内容。怎么解决呢?大佬们想到了一招,就是将类加载器对象放到当前线程中,需要的时候直接从线程中获取。喜欢看源码的童鞋不难发现,spring源码中也大量使用了这种方式。这样既不打破双亲委派模型,又实现了功能。不得不说大佬还是大佬啊.....
Answer 3:第三个问题其实作者已经给出了解释,就在bootstrap启动类的最上面有注释。
/**
* Bootstrap loader for Catalina. This application constructs a class loader
* for use in loading the Catalina internal classes (by accumulating all of the
* JAR files found in the "server" directory under "catalina.home"), and
* starts the regular execution of the container. The purpose of this
* roundabout approach is to keep the Catalina internal classes (and any
* other classes they depend on, such as an XML parser) out of the system
* class path and therefore not visible to application level classes.
*/
注意第四行,这种保持Catalina的内部类的迂回的方式,目的是为了让Catalina类(和其他Catalina依赖的类,例如xml解析类)对其他类路径和对其他应用程序类不可见。 这么解释我相信大多数看官还是一头雾水,哈哈哈~
我是这么理解的,为了保证Bootstrap类的应用隔离,需要在初始化的时候不依赖任何其他的类,也就是定义属性的时候除了jdk原生的类之外不能有 new Class()这种方式出现。所以在定义属性的时候只能用Object类型定义。如果直接用new的方式的话必须在定义的时候指定类型。这样保证了隔离性,出色的完成了解耦功能,这是个人见解,观众大佬们要是感觉不对还希望能指出来共同进步哈,别藏着掖着~
这么讲解会不会太细致了,然后大家看的不耐烦了呢,请各位给点意见哦。
下一篇介绍start方法,也就是真正开始初始化各个组件的过程。