由浅入深--探究Tomcat9.0之--入门篇2

再次强调一下,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的类加载器实现图。

Tomcat类加载器原型图标题

                            图片转自 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方法,也就是真正开始初始化各个组件的过程。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值