看到这个标题可能很多同学感觉这东西好像离自己的开发工作很遥远,但其实并没有想象的那么遥远。想一想为啥我们JSP修改之后不需要重启?它的热更新是怎么实现的?如果我们一个tomcat里面跑了10个SSM架构的项目,怎样节约内存?接下来跟着源码时代老师一起来看一下。
Tomcat是我们从一开始接触Java Web就认识的一个web服务器,它是由Java语言编写的,主要的文件夹就是bin,conf,lib,logs,webapps这5个文件夹
Bin,是我们启动,关闭tomcat的命令所在的文件夹
Conf,是我们tomcat相应配置所在的地方,在我们最早学习部署的时候,就有一种直接在conf中修改server.xml以及session过期时间之类的。
Lib,是tomcat所需依赖所在的文件夹,在之前5.x的版本中,并不直接是lib文件夹,而是分成了3个,common,server和shared三个文件夹,在下面我们来详细说一下这3个被移除掉的东西对于现在的我们还有什么意义。
Logs,是tomcat运行中的日志记录,当我们需要排查线上问题时,很多时候会需要通过查阅日志进行错误定位。
Webapps,这个我们应该是最熟悉的,因为我们部署项目都是扔到webapps里面然后启动tomcat
Tomcat也是使用Java编写的一个web服务器,需要依赖Java环境,也就是说它的运行是需要基于JVM的,而用到JVM也就必然会使用类加载器来进行类加载。
Tomcat的类加载架构如下图所示
灰色背景的即我们之前提到过的JVM中原生的类加载器,分别负责的工作范围大家可以通过上一篇文章回顾。而往下我们可以看到有Conmmon,Catalina,Shared,WebApp,JSP这几种类加载器,在我们讲解每个类加载器的作用前,先说一下tomcat 5.x的架构,便于我们理解。
在tomcat 5.x的时候,并不是一个lib包,而是common,server和shared这三个文件夹,按照双亲委托机制,不同的类加载器负责不同的范围,上图的Common类加载器负责/common/*,Catalina类加载器负责/server/*,Shared类加载器负责/shared/*,而WebAppClassLoader就是我们webapps文件夹下每个项目对应的类加载器了。
/common/*中的类可以被tomcat和所有的web应用程序共同使用。
/server/*中的类可以被tomcat使用,而对web应用程序不可见。
/shared/*中的类可以被web应用程序共同使用,而对tomcat不可见。
看到这里,上面问的第三个问题,如果有10个SSM架构的项目,怎么节约内存。答案就是可以通过把SSM的jar包放到/shared/文件夹中,整个tomcat运行只有这一份,所有web项目共用它来实现。
那么问题又来了,现在没这些文件夹,只有一个lib包了,又该怎么办。
Lib包现在相当于是common,我们把jar包放到common中虽然可以达到效果,但感觉会很混乱,因为这里面有tomcat运行所必须的依赖。所以我们需要把shared用上。
我们打开conf/Catalina.properties文件,可以看到这么一个默认的配置。
其中common.loader已经有值了,而server.loader和shared.loader是没有值的。对应上面我们说到的3个文件夹,想必各位同学应该是知道这每项代表的意思了。
那么我们可以直接在tomcat中建立shared文件夹,将需要共享的jar包放到文件夹中,然后配置shared.loader的值为文件夹的路径,就可以用上了。
在我们开发过程中,如果使用的tomcat(或者其他一些主流的web服务器)和jsp的话,我们习惯于修改jsp之后直接切换到浏览器进行一次刷新,看改动是否生效,并不会去重启项目,这就和我们前面说到的JasperLoader相关了。
WebAppClassLoader是每个web项目有一个,如我们举例所说的10个项目,那么就有10个WebAppClassLoader,但JasperLoader就不止于此了。
我们有一个jsp文件,就有一个JasperLoader。
为什么需要这么多?
我们的JSP,大家都知道本质上是个Servlet,也就是以字节码文件的形式存在。在JSP文件被编译后,我们会得到一个同名_jsp.class文件,然后被类加载器加载到JVM中使用。当服务器检测到JSP被修改后,会替换掉该JSP当前的JasperLoader实例,然后当再次访问到这个JSP页面时,触发它的编译和类加载操作,此时就会再次创建一个JasperLoader用来加载JSP编译得出的字节码文件。
双亲委托机制并不是强制的要求,只是作为一种推荐的方式,在面对一些特殊的情况下,可能需要破坏这种机制。
这里所说的破坏并不见的是一种贬义的行为,换成突破二字可能会更好。
双亲委托机制存在一种缺陷,基础类加载器只能加载自己指定范围内的类,对于用户提供的类是没法加载的,以至于如果基础类需要用到用户类,那就无法加载。比如JDBC,我们都知道这是Java提供了一个规范,由不同的厂商去实现,这个规范通过接口体现,作为基础类,而厂商的实现是驱动包,放在我们项目的依赖包中。
我们可以使用DriverManager.getConnection(url,user,password)直接去获取连接,而不手动去做贾琏欲执事中的第一步,使用Class.forName()去加载驱动。这样一来,DriverClassLoader会自动去寻找并加载驱动。如果严格按照双亲委托机制,DriverManager是java.sql包中的,属于BootStrapClassLoader的范围,它只能看到这个文件夹中的类,而我们的驱动包是不在这里面的,就无法找到类,也就没法去加载了。那实际是怎么处理的呢?
我们可以看到这是getConnection重载的方法,我们直接传入URL,user和password实际就是重载的这个方法。在这里我们可以看到获取了调用者的类加载器,并且判断了callerCL为null。同学们想一下,会有类没有类加载器吗?为null并不是说它没有类加载器,而是因为它的类加载器是BootStrapClassLoader,这是由C++语言写的,无法用Java对象表示。好,既然知道你是BootStrapClassLoader加载的了,那我就给你替换掉,替换成当前线程的线程上下文类加载器。这个类是在线程创建的时候,从父线程继承或者默认直接取ApplicationClassLoader。换成ApplicationClassLoader了,那我们项目依赖的驱动包是不是也就很轻松的可以扫描并加载了?当然,如果你本身调用者所属的类加载器就是Application Class Loader那都不用去线程中取了。
那为什么说这是对双亲委托机制的破坏呢?因为我们一向是从下至上的请求加载,而这次BootStrapClassLoader范围内的基础类直接请求了很底端的ApplicationClassLoader去加载,逆向打通了加载的顺序。
这里只是用JDBC举了个例子,各位同学可以自己想想,是不是其他由Java提供规范,而不同厂商提供实现的技术也会是这种实现的原理呢?