关于java:学习Tomcat六之类加载器

通过后面的文章咱们晓得,Tomcat的申请最终都会交给用户配置的servlet实例来解决。Servlet类是配置在配置文件中的,这就须要类加载器对Servlet类进行加载。Tomcat容器自定义了类加载器,有以下非凡性能:1. 在载入类中指定某些规定;2.缓存曾经载入的类;3.实现类的预加载。 本文会对Tomcat的类加载器进行具体介绍。

Java类加载双亲委派模型

Java类加载器是用户程序和JVM虚拟机之间的桥梁,在Java程序中起了至关重要的作用,对于其具体实现能够参考了java官网文档对于虚拟机加载的教程,点此中转官网参考文档。java中的类加载默认是采纳双亲委派模型,即加载一个类时,首先判断本身define加载器有没有加载过此类,如果加载了间接获取class对象,如果没有查到,则交给加载器的父类加载器去反复下面过程。我在另外一篇文章中具体介绍了Java的类加载机制,此处不做具体介绍。

Loader接口

在载入Web应用程序中须要的servlet类及其相干类时要恪守一些明确的规定,例如应用程序中的servlet只能援用部署在WEB-INF/classes目录及其子目录下的类。然而,servlet类不能拜访其它门路中的类,即便这些累蕴含在运行以后Tomcat的JVM的CLASSPATH环境变量中。此外,servlet类只能拜访WEB-INF/LIB目录下的库,其它目录的类库均不能拜访。Tomcat中的载入器值得是Web应用程序载入器,而不仅仅是类载入器,载入器必须实现Loader接口。Loader接口的定义如下所示:

public interface Loader {

    public void backgroundProcess();
    public ClassLoader getClassLoader();
    public Context getContext();
    public void setContext(Context context);
    public boolean getDelegate();
    public void setDelegate(boolean delegate);
    public void addPropertyChangeListener(PropertyChangeListener listener);
    public boolean modified();
    public void removePropertyChangeListener(PropertyChangeListener listener);
}

后台任务:Loader接口须要进行在servlet类变更的时候实现类的从新加载,这个工作就是在backgroundProcess()中实现的,WebApploader中backgroundProcess()的实现如下所示。能够看到,当Context容器开启了Reload性能并且仓库变更的状况下,Loaders会先把类加载器设置为Web类加载器,重启Context容器。重启Context容器会重启所有的子Wrapper容器,会销毁并从新创立servlet类的实例,从而达到动静加载servlet类的目标。

    @Override
    public void backgroundProcess() {
        Context context = getContext();
        if (context != null) {
            if (context.getReloadable() && modified()) {
                ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
                try {
                    Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
                    context.reload();
                } finally {
                    Thread.currentThread().setContextClassLoader(originalTccl);
                }
            }
        }
    }

类加载器:Loader的实现中,会应用一个自定义类载入器,它是WebappClassLoader类的一个实例。能够应用Loader接口的getClassLoader()办法来获取Web载入器中的ClassLoader的实例。默认的类加载器的实现有两种种:ParallelWebappClassLoader和WebappClassLoader

Context容器:Tomcat的载入器通常会与一个Context级别的servelt容器相关联,Loader接口的getContainer()办法和setContainer()办法用来将载入器和某个servlet容器关联。如果Context容器中的一个或者多个类被批改了,载入器也能够反对对类的重载。这样,servlet程序员就能够从新编译servlet类及其相干类,并将其从新载入而不须要重新启动Tomcat。Loader接口应用modified()办法来反对类的主动重载。

类批改检测:在载入器的具体实现中,如果仓库中的一个或者多个类被批改了,那么modified()办法必须放回true,能力提供主动重载的反对

父载入器:载入器的实现会指明是否要委托给父类的载入器,能够通过setDelegate()和getDelegate办法配置。

WebappLoader类

Tomcat中惟一实现Loader接口的类就是WebappLoader类,其实例会用作Web利用容器的载入器,负责载入Web应用程序中所应用的类。在容器启动的时候,WebApploader会执行以下工作:

  • 创立类加载器
  • 设置仓库
  • 设置类的门路
  • 设置拜访权限
  • 启动新线程来反对主动重载

创立类加载器

为了实现类加载性能,WebappLoader会依照配置创立类加载器的实例,Tomcat默认有两品种加载器:WebappClassLoader和ParallelWebappClassLoader,默认状况下应用ParallelWebappClassLoader作为类加载器。用户能够通过setLoaderClass()设置类加载器的名称。WebappLoader创立类加载器的源码如下所示,咱们能够看到类加载器的实例必须是WebappClassLoaderBase的子类。

    private WebappClassLoaderBase createClassLoader()
        throws Exception {

        if (classLoader != null) {
            return classLoader;
        }

        if (ParallelWebappClassLoader.class.getName().equals(loaderClass)) {
            return new ParallelWebappClassLoader(context.getParentClassLoader());
        }

        Class<?> clazz = Class.forName(loaderClass);
        WebappClassLoaderBase classLoader = null;

        ClassLoader parentClassLoader = context.getParentClassLoader();

        Class<?>[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor<?> constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoaderBase) constr.newInstance(args);

        return classLoader;
    }

设置仓库

WebappLoader会在启动的时候调用类加载器的初始化办法,类加载器在初始化的时候会设置类加载的仓库地址。默认的仓库地址为”/WEB-INF/classes”和”/WEB-INF/lib”。类加载器初始化源码如下所示:

    @Override
    public void start() throws LifecycleException {

        state = LifecycleState.STARTING_PREP;

        WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
        for (WebResource classes : classesResources) {
            if (classes.isDirectory() && classes.canRead()) {
                localRepositories.add(classes.getURL());
            }
        }
        WebResource[] jars = resources.listResources("/WEB-INF/lib");
        for (WebResource jar : jars) {
            if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
                localRepositories.add(jar.getURL());
                jarModificationTimes.put(
                        jar.getName(), Long.valueOf(jar.getLastModified()));
            }
        }

        state = LifecycleState.STARTED;
    }

设置类门路

设置类门路是在初始化的时候调用setClassPath()办法实现的(源码如下)。setClassPath()办法会在servlet上下文中为Jasper JSP编译器设置一个字符串类型的属性来指明类门路信息。此处不具体介绍JSP相干内容。

  private void setClassPath() {

        // Validate our current state information
        if (context == null)
            return;
        ServletContext servletContext = context.getServletContext();
        if (servletContext == null)
            return;

        StringBuilder classpath = new StringBuilder();

        // Assemble the class path information from our class loader chain
        ClassLoader loader = getClassLoader();

        if (delegate && loader != null) {
            // Skip the webapp loader for now as delegation is enabled
            loader = loader.getParent();
        }

        while (loader != null) {
            if (!buildClassPath(classpath, loader)) {
                break;
            }
            loader = loader.getParent();
        }

        if (delegate) {
            // Delegation was enabled, go back and add the webapp paths
            loader = getClassLoader();
            if (loader != null) {
                buildClassPath(classpath, loader);
            }
        }

        this.classpath = classpath.toString();

        // Store the assembled class path as a servlet context attribute
        servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath);
    }

设置拜访权限

若是运行Tomcat的时候,应用了平安管理器,则setPermissions()办法会为类载入器设置拜访相干目录的权限,比方只能拜访WEB-INF/classes和WEB-INF/lib的目录。若是没有应用平安管理器,则setPermissions()办法只是简略地返回,什么也不做。其源码如下:


    /**
     * Configure associated class loader permissions.
     */
    private void setPermissions() {

        if (!Globals.IS_SECURITY_ENABLED)
            return;
        if (context == null)
            return;

        // Tell the class loader the root of the context
        ServletContext servletContext = context.getServletContext();

        // Assigning permissions for the work directory
        File workDir =
            (File) servletContext.getAttribute(ServletContext.TEMPDIR);
        if (workDir != null) {
            try {
                String workDirPath = workDir.getCanonicalPath();
                classLoader.addPermission
                    (new FilePermission(workDirPath, "read,write"));
                classLoader.addPermission
                    (new FilePermission(workDirPath + File.separator + "-",
                                        "read,write,delete"));
            } catch (IOException e) {
                // Ignore
            }
        }

        for (URL url : context.getResources().getBaseUrls()) {
           classLoader.addPermission(url);
        }
    }

开启新线程执行类的从新载入

WebappLoader类反对主动重载性能。如果WEB-INF/classes目录或者WEB-INF/lib目录下的某些类被从新编译了,那么这个类会主动从新载入,而无需重启Tomcat。为了实现此目标,WebappLoader类应用一个线程周期性的查看每个资源的工夫戳。间隔时间由变量checkInterval指定,单位为s,默认状况下,checkInterval的值为15s,每隔15s会查看顺次是否有文件须要主动从新载入。顶层容器在启动的时候,会启动定时线程池循环调用backgroundProcess工作。

    protected void threadStart() {
        if (backgroundProcessorDelay > 0
                && (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
                && (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
            if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
                // There was an error executing the scheduled task, get it and log it
                try {
                    backgroundProcessorFuture.get();
                } catch (InterruptedException | ExecutionException e) {
                    log.error(sm.getString("containerBase.backgroundProcess.error"), e);
                }
            }
            backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
                    .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
                            backgroundProcessorDelay, backgroundProcessorDelay,
                            TimeUnit.SECONDS);
        }
    }

    @Override
    public void backgroundProcess() {
        Context context = getContext();
        if (context != null) {
            if (context.getReloadable() && modified()) {
                ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
                try {
                    Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
                    context.reload();
                } finally {
                    Thread.currentThread().setContextClassLoader(originalTccl);
                }
            }
        }
    } 

WebappClassLoader类加载器

Web应用程序中负责载入类的类载入器有两种:ParallelWebappClassLoader和WebappClassLoaderBase,二者实现大同小异,本节以WebappClassLoader类加载器为例,介绍Tomcat的类加载器。

WebappClassLoader的设计方案思考了优化和平安两方面。例如,它会缓存之前曾经载入的类来晋升性能,还会缓存加载失败的类的名字,这样,当再次申请加载同一个类的时候,类加载器就会间接抛出ClassNotFindException异样,而不是再次去查找这个类。WebappClassLoader会在仓库列表和指定的JAR文件中搜寻须要在载入的类。

类缓存

为了达到更好的性能,WebappClassLoader会缓存曾经载入的类,这样下次再应用该类的时候,会间接从缓存中获取。由WebappClassLoader载入的类都会被视为资源进行缓存,对应的类为“ResourceEntry”类的实例。ResourceEndty保留了其所代表的class文件的字节流、最初一次批改日期,Manifest信息等。如下为类加载过程中读取缓存的局部代码和ResourceEntry的定义源码。

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    // 省略局部逻辑
    // (0) Check our previously loaded local class cache
    clazz = findLoadedClass0(name);
    if (clazz != null) {
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
        return clazz;
    }
     // 省略局部逻辑
}

protected Class<?> findLoadedClass0(String name) {

    String path = binaryNameToPath(name, true);

    ResourceEntry entry = resourceEntries.get(path);
    if (entry != null) {
        return entry.loadedClass;
    }
     return null;
}


public class ResourceEntry {
    /**
     * The "last modified" time of the origin file at the time this resource
     * was loaded, in milliseconds since the epoch.
     */
    public long lastModified = -1;

    /**
     * Loaded class.
     */
    public volatile Class<?> loadedClass = null;
}

载入类

载入类的时候,WebappClassLoader要遵循如下规定:

  1. 因为所有曾经载入的类都会缓存起来,所以载入类的时候要先查看本地缓存。
  2. 若本地缓存没有,则查看父类加载器的缓存,调用ClassLoader接口的findLoadedClass()办法。
  3. 若两个缓存总都没有,则应用零碎类加载器进行加载,避免Web应用程序中的类笼罩J2EE中的类。
  4. 若启用了SecurityManager,则查看是否容许载入该类。若该类是禁止载入的类,抛出ClassNotFoundException异样。
  5. 若关上了标记位delegate,或者待载入的在类不能用web类加载器加载的类,则应用父类加载器来加载器来加载相干类。如果父类加载器为null,则应用零碎类加载器。
  6. 从以后仓库载入类。
  7. 以后仓库没有须要载入的类,而且delegate敞开,则是用父类载入器来载入相干的类。
  8. 若没有找到须要加载的类,则抛出ClassNotFindException。

Tomcat类加载构造

Tomcat容器在启动的时候会初始化类加载器,Tomcat的类加载器分为四种类型:Common类加载器,Cataline类加载器和Shared类加载器,此外每个利用都会有本人的Webapp类加载器,也就是咱们上文介绍的WebappClassLoader,四者之间的关系如下所示。

Common类加载器,Cataline类加载器和Shared类加载器会在Tomcat容器启动的时候就初始化实现,初始化代码如下所示:

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


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

而Webapp类加载器则是在Context容器启动时候有WebappLoader初始化,Webapp类加载器的父类加载器是Tomcat容器在初始化阶段通过反射设置的,反射设置父类加载器的源码如下所示:

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

Tomcat类加载构造的目标

  1. 一个web容器可能须要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因而要保障每个应用程序的类库都是独立的,保障互相隔离。所以每个利用须要本身的Webapp类加载器。
  2. 部署在同一个web容器中雷同的类库雷同的版本能够共享。否则,如果服务器有10个应用程序,那么要有10份雷同的类库加载进虚拟机。所以须要Shared类加载器
  3. web容器也有本人依赖的类库,不能于应用程序的类库混同。基于平安思考,应该让容器的类库和程序的类库隔离开来。所以须要Cataline类加载器。
  4. web容器要反对jsp的批改,咱们晓得,jsp 文件最终也是要编译成class文件能力在虚拟机中运行,但程序运行后批改jsp曾经是司空见惯的事件,否则要你何用? 所以,web容器须要反对 jsp 批改后不必重启。

还有最初一个类的共享的问题,如果十个web利用都引入了spring的类,因为web类加载器的隔离,那么对内存的开销是很大的。此时咱们能够想到shared类加载器,咱们必定都会抉择将spring的jar放于shared目录底下,然而此时又会存在一个问题,shared类加载器是webapp类加载器的parent,若spring中的getBean办法须要加载web利用底下的类,这种过程是违反双亲委托机制的。

突破双亲委托机制的枷锁:线程上下文类加载器线程上下文类加载器是指的以后线程所用的类加载器,能够通过Thread.currentThread().getContextClassLoader()取得或者设置。在spring中,他会抉择线程上下文类加载器去加载web利用底下的类,如此就突破了双亲委托机制。

参考文档列表

  • tomcat学习|tomcat中的类加载器
  • 深刻了解Tomcat(五)类加载机制

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

本文最先公布至微信公众号,版权所有,禁止转载!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理