Tomcat 9 源码分析(2)— 类加载体系

Java虚拟机主要类加载器

  • Bootstrap Loader(启动类加载器):加载lib目录下或者System.getProperty("sun.boot.class.path")、或者-XBootclasspath所指定的路径或jar
  • Extended Loader(扩展类加载器):加载lib\ext目录下或者System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:

    java -Djava.ext.dirs=d:\projects\testproject\classes HelloWorld

  • System Loader(系统类加载器):加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如:

    java -cp ./testsoft/classes HelloWorld

System Loader 也可以称为应用类加载器 Application Loader

Tomcat的类加载体系

除了JVM规范提到的以上三种类加载器之外,Tomcat还实现了自身的类加载体系。各个类加载器之间不是继承关系,而是一种委派关系

avatar

  • ClassLoader:Java提供的类加载器抽象类,用户自定义的类加载器需要继承实现
  • commonLoader:Tomcat最基本的类加载器,加载路径中class可以被Tomcat容器本身以及各个Webapp访问
  • catalinaLoader(serverLoader):Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有的Webapp课件,但是对Tomcat容器不可见
  • WebappClassLoader:各个Webapp私有的累加载器,加载路径中的class只对当前Webapp可见

源码分析

commonLoader、catalinaLoader和SharedLoader是在Tomcat容器初始化的过程刚刚开始,即调用Bootstrap.init()时创建的。catalinaLoader会被设置为Tomcat主线程的线程上下文类加载器,并且使用catalinaLoader加载Tomcat容器自身class

org.apache.catalina.startup.Bootstrap.init()

public void init() throws Exception {
    //  初始化catalina.properties中的commonLoader,serverLoader,sharedLoader
    //  如果catalina.properties中的server.loader和sharedLoader为空则以common.Loader替代
    //  查找各个loader中的系统变量,其中包含了以","分隔的文件路径(URL、JAR、目录等),称之为repository
    //  调用ClassLoaderFactory.createClassLoader()产生classLoader
    initClassLoaders();
    //  把生成的catalinaLoader设置为当前线程上下文的classloader
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    //  线程安全加载class
   classSecurityClassLoad.securityClassLoad(catalinaLoader);
   // 省略后面的code
}

Bootstrap.main()内对ClassLoader的处理如下:

  1. 初始化commonLoader、catalinaLoader、sharedLoader
  2. 将catalinaLoader设置为Tomcat主线程的线程上下文加载器
  3. 线程安全加载class

初始化类加载器

org.apache.catalina.startup.Bootstrap.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);
    }
}

initClassLoader调用createClassLoader()创建commonLoader、catalinaLoader和sharedLoader

org.apache.catalina.startup.Bootstarp.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
        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);
}

在最后调用ClassLoaderFactory.createClassLoader()创建classLoader

org.apache.catalina.startup.ClassLoaderFactory.createClassLoader()

//  省略部分code
return AccessController.doPrivileged(
        (PrivilegedAction<URLClassLoader>) () -> {
            if (parent == null)
                return new URLClassLoader(array);
            else
                return new URLClassLoader(array, parent);
        });

其内代码的步骤为:

  1. 定位资源路径与资源类型
  2. 使用ClassLoaderFactory创建类加载器UrlClassLoader(旧版本Tomcat为创建org.apache.catalina.loader.StandardClassLoader 新版本中已被抛弃)

注:Tomcat默认会指定commonLoader(通过catalina.properties的common.loader属性)

tomcat/conf/catalina.properties

//common.loader:tomcat自身和各web服务都会用到的类
//server.loader:tomcat自身会用到的类 如果为空则由commonLoader替代
//shared.loader:各web服务会用到的类 如果为空则由commonLoader替代
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=

安全加载class

Bootstrap.init()内完成了初始化类加载器后便是安全加载class

org.apache.catalina.security.SecurityClassLoad.securityClassLoad()

public static void securityClassLoad(ClassLoader loader) throws Exception {
    securityClassLoad(loader, true);
}


static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception {

    if (requireSecurityManager && System.getSecurityManager() == null) {
        return;
    }
    //  Tomcat核心class - org.apache.catalina.core
    loadCorePackage(loader);
    //  Tomcat对底层socket连接数据的封装框架 - org.apache.coyote
    loadCoyotePackage(loader);
    //  Tomcat提供的根据名称寻找类以及一个日志处理 - org.apache.catalina.loader.WebappClassLoaderBase内的两个内部类
    loadLoaderPackage(loader);
    //  Tomcat的Realm实现,对多次验证失败的用户提供锁定功能 - org.apache.catalina.realm
    loadRealmPackage(loader);
    //  Tomcat的默认Servlet - org.apache.catalina.servlets.DefaultServlet
    loadServletsPackage(loader);
    //  Tomcat有关session的class - org.apache.catalina.session
    loadSessionPackage(loader);
    //  Tomcat工具类的class - org.apache.catalina.util
    loadUtilPackage(loader);
    //  处理Cookie - javax.servlet.http.Cookie
    loadJavaxPackage(loader);
    //  Tomcat Connector(连接器)组件 - org.apache.catalina.connector
    loadConnectorPackage(loader);
    //  Tomcat其他工具类的class - org.apache.tomcat.util
    loadTomcatPackage(loader);
}

securityClassLoad()内执行的方法作用如以上的注释

securityClassLoad主要负责加载Tomcat容器所需的class,具体不详情描述自行查看

WebappClassLoader的实现

在StandardContext的初始化方法中,最后会调用WebappLoader的start方法,start中又调用了startInternal方法

org.apache.catalina.core.StandardContext.startInternal()```

/**
 * Start this component and implement the requirements
 * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
 *
 * @exception LifecycleException if this component detects a fatal error
 *  that prevents this component from being used
 */
@Override
protected synchronized void startInternal() throws LifecycleException {
     
     //  省略前面其余代码
     
     if (getLoader() == null) {
         WebappLoader webappLoader = new WebappLoader();
         webappLoader.setDelegate(getDelegate());
         setLoader(webappLoader);
     }
    
    //  省略中间其余代码
    
    // Start our subordinate components, if any
    Loader loader = getLoader();
    if (loader instanceof Lifecycle) {
        ((Lifecycle) loader).start();
    }
    
    //  省略后面其余代码
    
}

org.apache.catalina.loader.WebappLoader.startInternal()

/**
 * Start associated {@link ClassLoader} and implement the requirements
 * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
 *
 * @exception LifecycleException if this component detects a fatal error
 *  that prevents this component from being used
 */
@Override
protected void startInternal() throws LifecycleException {

    if (log.isDebugEnabled())
        log.debug(sm.getString("webappLoader.starting"));

    if (context.getResources() == null) {
        log.info(sm.getString("webappLoader.noResources", context));
        setState(LifecycleState.STARTING);
        return;
    }

    // Construct a class loader based on our current repositories list
    try {
        
        classLoader = createClassLoader();
        classLoader.setResources(context.getResources());
        classLoader.setDelegate(this.delegate);

        // Configure our repositories
        setClassPath();

        setPermissions();

        classLoader.start();

        String contextName = context.getName();
        if (!contextName.startsWith("/")) {
            contextName = "/" + contextName;
        }
        ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
                classLoader.getClass().getSimpleName() + ",host=" +
                context.getParent().getName() + ",context=" + contextName);
        Registry.getRegistry(null, null)
            .registerComponent(classLoader, cloname, null);

    } catch (Throwable t) {
        t = ExceptionUtils.unwrapInvocationTargetException(t);
        ExceptionUtils.handleThrowable(t);
        throw new LifecycleException(sm.getString("webappLoader.startError"), t);
    }

    setState(LifecycleState.STARTING);
}

WebappLoader.startInternal()调用了createClassLoader()创建类加载器

org.apache.catalina.loader.WebappLoader.createClassLoader()

/**
 * Create associated classLoader.
 */
private WebappClassLoaderBase createClassLoader()
    throws Exception {

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

    if (parentClassLoader == null) {
        parentClassLoader = context.getParentClassLoader();
    } else {
        context.setParentClassLoader(parentClassLoader);
    }

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

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

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

    return classLoader;
}

这里的parentClassLoader实际就是sharedLoader,至此整个Tomcat的类加载体系构建完毕

Tomcat重写了loadClass使类加载得以隔离

org.apache.catalina.loader.WebappClassLoaderBase.loadClass()

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

    synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
        if (log.isDebugEnabled())
            log.debug("loadClass(" + name + ", " + resolve + ")");
        Class<?> clazz = null;

        // Log access to stopped class loader
        checkStateForClassLoading(name);

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

        // (0.1) Check our previously loaded class cache
        clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
        if (clazz != null) {
            if (log.isDebugEnabled())
                log.debug("  Returning class from cache");
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // (0.2) Try loading the class with the system class loader, to prevent
        //       the webapp from overriding Java SE classes. This implements
        //       SRV.10.7.2
        String resourceName = binaryNameToPath(name, false);

        ClassLoader javaseLoader = getJavaseClassLoader();
        boolean tryLoadingFromJavaseLoader;
        try {
            // Use getResource as it won't trigger an expensive
            // ClassNotFoundException if the resource is not available from
            // the Java SE class loader. However (see
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
            // details) when running under a security manager in rare cases
            // this call may trigger a ClassCircularityError.
            // See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
            // details of how this may trigger a StackOverflowError
            // Given these reported errors, catch Throwable to ensure any
            // other edge cases are also caught
            URL url;
            if (securityManager != null) {
                PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
                url = AccessController.doPrivileged(dp);
            } else {
                url = javaseLoader.getResource(resourceName);
            }
            tryLoadingFromJavaseLoader = (url != null);
        } catch (Throwable t) {
            // Swallow all exceptions apart from those that must be re-thrown
            ExceptionUtils.handleThrowable(t);
            // The getResource() trick won't work for this class. We have to
            // try loading it directly and accept that we might get a
            // ClassNotFoundException.
            tryLoadingFromJavaseLoader = true;
        }

        if (tryLoadingFromJavaseLoader) {
            try {
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }

        // (0.5) Permission to access this class when using a SecurityManager
        if (securityManager != null) {
            int i = name.lastIndexOf('.');
            if (i >= 0) {
                try {
                    securityManager.checkPackageAccess(name.substring(0,i));
                } catch (SecurityException se) {
                    String error = sm.getString("webappClassLoader.restrictedPackage", name);
                    log.info(error, se);
                    throw new ClassNotFoundException(error, se);
                }
            }
        }

        boolean delegateLoad = delegate || filter(name, true);

        // (1) Delegate to our parent if requested
        if (delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader1 " + parent);
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }

        // (2) Search local repositories
        if (log.isDebugEnabled())
            log.debug("  Searching local repositories");
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from local repository");
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // (3) Delegate to parent unconditionally
        if (!delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader at end: " + parent);
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }
    }

    throw new ClassNotFoundException(name);
}

WebappClassLoaderBase加载class步骤:

  1. 从之前加载的class的缓存中查找
  2. 委托sun.misc.Launcher$AppClassLoader加载class
  3. 安全检查通过后,委托父类加载器SharedClassLoader加载Class
  4. WebappClassLoader自己加载class

总结

WebappClassLoaderBase的findClass实现主要调用findClassInternal方法来加载webapp自身路径下的class

posted @ 2021-03-30 20:01  Kaiyko  阅读(329)  评论(0)    收藏  举报