Tomcat 源码分析(三)

面临的问题

一个健全的 Web 服务器,要解决以下几个问题:

  1. 部署在同一个服务器上的两个 web 应用程序所使用的 Java 类库需要实现相互隔离,因为两个 web 应用程序可能会使用到同一个类库的不同版本,因此需要保证两个应用程序的类库是可以互相独立使用的。
  2. 部署在同一个服务器上的两个 web 应用程序所使用的 Java 类库可以实现共享。当两个 web 应用程序依赖了同一个类库的相同版本,如果隔离开来,那么服务器所处的虚拟机的方法区就会很容易出现过度膨胀的风险。
  3. 服务器需要尽可能保证自身的安全不受部署的web应用程序的影响。许多web服务器自身也是使用java语言实现的,服务器本身也有类库依赖,基于安全考虑服务器所使用的类库,应该与web应用程序的类库相互独立。
  4. 支持jsp的热替换,jsp文件会生成servlet类,在开发过程中修改评率比较高,因为需要支持人替换,保证修改后不需要重启服务器。

解决办法

在tomcat 目录中有四个类库目录/common/*/server/*/shared/*和各个web应用程序自身的/WEB-INF/*目录:

  • /common/*: 类库可以被 tomcat 和所有的 Web 应用程序共同使用;
  • /server/*:类库只能被 tomcat 使用;
  • /shared/*:类库可以被所有 Web 应用程序共同使用,但 Tomcat 自身无法使用;
  • /WEB-INF/*:只能被对应的 Web 应用程序使用,其他 Web 应用程序以及tomcat 不能使用。

为支持这套结构,tomcat 定义了多个类加载器按照经典的双亲委派模型实现:
tomcat类加载器

源码分析

简单看一下 Bootstrap 类的源码
main 方法:

    public static void main(String args[]) {

        synchronized (daemonLock) {
            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);
            }
        }
		......

    }

进入 Bootstrap 的 init 方法中:

public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);
        ......

    }

进入 initClassLoaders 方法

private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                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);
        }
    }

从这里可以看出来commonLoader 是catalinaLoader 和sharedLoader 的父加载器,接下来看看具体的classLoader的创建方式:

 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
            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);
    }

可以看出来Common ClassLoader,Shared ClassLoader,Catalina ClassLoader对应的是初始化时使用不同名字的三个URLClassLoader。

上文有分析到,sharedClassLoader是 webAppClassLoader的父加载器,接下来分析一下这个关系是如何构建的:

tomcat 在init方法中将Shared ClassLoader 传入Catalina对象的setParentClassLoader 方法中:

 public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        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);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;

    }

在部署 web 应用后经过重重调用,最终会创建 StandardContext 对象,在层层调用过程中,会向内传递 Catalina对象的 parentClassloader,而 StandardContext 的 startInternal 方法会建立起Shared ClassLoader 与 webClassLoader之间的关系:

      WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
      webappLoader.setDelegate(getDelegate());
      setLoader(webappLoader);

总结一下当前分析的结果:在 Tomcat 中存在 common、cataina、shared 三个公共的 classloader ,而针对每个webapp,都有对应的context(对应代码中的StandardContext 类),也都有自己的 WebappClassLoader 实例来加载每个应用自己的类和类库,该类加载器的 parent 即是 Shared ClassLoader 。

通过这样的类加载器的组合结构,已经可以解决文档开篇所提到的前三个问题。

jsp 的热替换

正如我们所知 JSP 是 Servlet 的一种特殊形式,每个 JSP 页面就是一个 Servlet 实例 ——JSP 页面由系统编译成 Servlet,Servlet 再负责响应用户请求。JSP 其实也是 Servlet 的一种简化,使用 JSP 时,其实还是使用 Servlet,因为 Web 应用中的每个 JSP 页面都会由 Servlet 容器生成对应的 Servlet。
既然需要编译成 servlet 文件那么就会存在 class,这些 class 则是通过 JasperLoader 加载的,下面做具体分析:
在 tomcat 内置的 web.xml 中,默认配置了 jspServlet,用于处理针对 jsp 的请求:

<servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

如上面的 xml 所示,监听 jsp 或者 jspx 为后缀的请求。
在请求 jsp 的时候,进入 jspServlet 的 service 方法,进入 jspServletWrapper 的 service 方法调用 JspCompilationContext 对象 ctxt 的 compile 方法进行编译 jsp 。
代码逻辑如下(编译部分已加注释):
jspServlet:

public void service (HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ......
        try {
            boolean precompile = preCompile(request);
            serviceJspFile(request, response, jspUri, precompile);
        } 
        ......

    }

private void serviceJspFile(HttpServletRequest request,
                                HttpServletResponse response, String jspUri,
                                boolean precompile)
        throws ServletException, IOException {

        JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
        ......
        try {
            wrapper.service(request, response, precompile);
        } catch (FileNotFoundException fnfe) {
            handleMissingResource(request, response, jspUri);
        }

    }

jspServletWrapper :

public void service(HttpServletRequest request,
                        HttpServletResponse response,
                        boolean precompile)
            throws ServletException, IOException, FileNotFoundException {

        Servlet servlet;

        try {

            if (ctxt.isRemoved()) {
                throw new FileNotFoundException(jspUri);
            }

            if ((available > 0L) && (available < Long.MAX_VALUE)) {
                if (available > System.currentTimeMillis()) {
                    response.setDateHeader("Retry-After", available);
                    response.sendError
                        (HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                         Localizer.getMessage("jsp.error.unavailable"));
                    return;
                }
                available = 0;
            }

            /*
             * 编译 jsp
             */
            if (options.getDevelopment() || mustCompile) {
                synchronized (this) {
                    if (options.getDevelopment() || mustCompile) {
                        ctxt.compile();
                        mustCompile = false;
                    }
                }
            } else {
                if (compileException != null) {
                    throw compileException;
                }
            }

            /*
             * 加载编译好的Servlet class
             */
            servlet = getServlet();

           ......

        try {
          ......

            /*
             * 使用编译好的 servlet 处理请求
             */
            if (servlet instanceof SingleThreadModel) {
               synchronized (this) {
                   servlet.service(request, response);
                }
            } else {
                servlet.service(request, response);
            }
        } 
        ......
    }

在编译jsp的方法中,首先判断当前已编译的内容是否已经过时,若过时则删除已编译好的文件,重新编译,否则不做处理

public void compile() throws JasperException, FileNotFoundException {
        createCompiler();
        // 判断是否过时
        if (jspCompiler.isOutDated()) {
            if (isRemoved()) {
                throw new FileNotFoundException(jspUri);
            }
            try {
                // 移除已编译的文件
                jspCompiler.removeGeneratedFiles();
                jspLoader = null;
                // 编译
                jspCompiler.compile();
                jsw.setReload(true);
                jsw.setCompilationException(null);
            } catch (JasperException ex) {
                jsw.setCompilationException(ex);
                if (options.getDevelopment() && options.getRecompileOnFail()) {
                    jsw.setLastModificationTest(-1);
                }
                throw ex;
            } catch (FileNotFoundException fnfe) {
                throw fnfe;
            } catch (Exception ex) {
                JasperException je = new JasperException(
                        Localizer.getMessage("jsp.error.unable.compile"),
                        ex);
                jsw.setCompilationException(je);
                throw je;
            }
        }
    }

过时的判断条件是当前jsp或者依赖的文件的最近修改时间与jsp文件实际的修改时间不一致时,则为过时:

public boolean isOutDated(boolean checkClass) {

        if (jsw != null
                && (ctxt.getOptions().getModificationTestInterval() > 0)) {

            if (jsw.getLastModificationTest()
                    + (ctxt.getOptions().getModificationTestInterval() * 1000) > System
                    .currentTimeMillis()) {
                return false;
            }
            jsw.setLastModificationTest(System.currentTimeMillis());
        }

        // 获取已编译的文件
        File targetFile;
        if (checkClass) {
            targetFile = new File(ctxt.getClassFileName());
        } else {
            targetFile = new File(ctxt.getServletJavaFileName());
        }
        // 已编译的文件不存在或者未被编译过,则直接认定为过时,需要编译
        if (!targetFile.exists()) {
            return true;
        }
        long targetLastModified = targetFile.lastModified();
        if (checkClass && jsw != null) {
            jsw.setServletClassLastModifiedTime(targetLastModified);
        }

        Long jspRealLastModified = ctxt.getLastModified(ctxt.getJspFile());
        if (jspRealLastModified.longValue() < 0) {
            return true;
        }
        // 判断 jsp 文件的时间修改时间与已经编译过得文件的最后修改时间是否一致
        if (targetLastModified != jspRealLastModified.longValue()) {
            if (log.isDebugEnabled()) {
                log.debug("Compiler: outdated: " + targetFile + " "
                        + targetLastModified);
            }
            return true;
        }
        if (jsw == null) {
            return false;
        }

        // 获取依赖的文件
        Map<String,Long> depends = jsw.getDependants();
        if (depends == null) {
            return false;
        }

        // 循环获取所有以来的文件判断是否有修改时间不一致的
        for (Entry<String, Long> include : depends.entrySet()) {
            try {
                String key = include.getKey();
                URL includeUrl;
                long includeLastModified = 0;
                if (key.startsWith("jar:jar:")) {
                    // Assume we constructed this correctly
                    int entryStart = key.lastIndexOf("!/");
                    String entry = key.substring(entryStart + 2);
                    try (Jar jar = JarFactory.newInstance(new URL(key.substring(4, entryStart)))) {
                        includeLastModified = jar.getLastModified(entry);
                    }
                } else {
                    if (key.startsWith("jar:") || key.startsWith("file:")) {
                        includeUrl = new URL(key);
                    } else {
                        includeUrl = ctxt.getResource(include.getKey());
                    }
                    if (includeUrl == null) {
                        return true;
                    }
                    URLConnection iuc = includeUrl.openConnection();
                    if (iuc instanceof JarURLConnection) {
                        includeLastModified =
                            ((JarURLConnection) iuc).getJarEntry().getTime();
                    } else {
                        includeLastModified = iuc.getLastModified();
                    }
                    iuc.getInputStream().close();
                }

                if (includeLastModified != include.getValue().longValue()) {
                    return true;
                }
            } catch (Exception e) {
                if (log.isDebugEnabled())
                    log.debug("Problem accessing resource. Treat as outdated.",
                            e);
                return true;
            }
        }

        return false;

    }

jsp 经过编译完成后生成的 class,则由 jasperLoader 加载进入虚拟机,进行初始化,然后处理请求:

 public Servlet getServlet() throws ServletException {
        if (getReloadInternal() || theServlet == null) {
            synchronized (this) {
                if (getReloadInternal() || theServlet == null) {
                    destroy();

                    final Servlet servlet;

                    try {
                        InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config);
                        // 加载、初始化
                        servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader());
                    } catch (Exception e) {
                        Throwable t = ExceptionUtils
                                .unwrapInvocationTargetException(e);
                        ExceptionUtils.handleThrowable(t);
                        throw new JasperException(t);
                    }

                    servlet.init(config);

                    if (theServlet != null) {
                        ctxt.getRuntimeContext().incrementJspReloadCount();
                    }

                    theServlet = servlet;
                    reload = false;
                }
            }
        }
        return theServlet;
    }
public ClassLoader getJspLoader() {
        if( jspLoader == null ) {
            jspLoader = new JasperLoader
                    (new URL[] {baseUrl},
                            getClassLoader(),
                            rctxt.getPermissionCollection());
        }
        return jspLoader;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值