类路径(classpath)的原理及在Tomcat中的使用

本文介绍了Java类路径(classpath)的基本概念和作用,讲解了其在命令行和IDE中的不同使用情况,以及如何通过工具查看类路径信息。此外,文章还探讨了在Tomcat中类路径的特殊处理,分析了Tomcat启动脚本和类加载器的工作原理,强调了类路径在应用间依赖隔离中的重要性。

每个Java开发者,从开始学习Java的那一天起,就认识了classpath。也许你只是不经意的输入了一个java HelloWorld 但这个时候,你已经无意中使用到了classpath。

说了半天,什么是classpath,有什么用呢?

概念

我们都知道,在JAVA的世界里,两个变更至关重要:pathclasspath 其中path指定了我们所使用的javacjava这些可招待程序的发行版具体路径,而classpath主要用来标识在执行java程序时加载类的位置。

Oracle官方的精确定义是这样的:

The class path is the path that the Java runtime environment searches for classes and other resource files.

即classpath是Java运行时查找class和其他资源文件的路径。

一般可以在java命令后以-classpath 或-cp的形式,来指定具体的classpath。而且,一般在系统中也不建议直接指定CLASSPATH这个环境变量,毕竟每个应用 的类路径是有区别的,在各个不同的应用中以-cp这种形式更容易使用。如果我们没有显式的指定-cp的时候,其默认值是一个点( .),代表当前路径,所以我们前面说 虽然你只是简单的输入一个java,后面跟了一个class的名称,但你已经在使用它了。

虽然说起来道理很简单,但还是会成为一些初学者,甚至一些所谓「资深」的开发者的难题。

原理

为什么看似简单的classpath,会成为开发中的一个拌脚石呢?

我们开发过程中,如果一直使用IDE进行开发,这些classpath的设置,已经由IDE代为完成,所以每次都是直接运行自己的程序,依赖的外部程序都是直接添加到所谓的 build-path里。而这些内容,在启动应用程序时,就会以classpath的形式提供。

而在命令行中运行Java程序时,这一切都需要手动进行配置,此时就会出现问题。有时是ClassNotFoundException,有时会是找不到或无法加载主类。这个时候,一定是类路径设置的问题,无法找到对应的类,或者是已经加载到的类路径中的这些class,没有包含main方法的类,或者jar文件中的MANIFAST.MF里没有Main-Class

那么,如何确定当前运行的Java应用加载了哪些资源到其类路径里,或者说它的classpath配置了一些什么内容呢? 这里有几个Java默认饮食的小工具,可以直接使用。

  1. jps

    这个命令我们在前面的文章里曾经介绍过。(你可能不知道的几个java小工具)通过jps -lv可以列出当前运行的所有Java进程,以及其详细的参数信息,这样我们的配置的classpath内容就罗列出来了。

  2. Jconsole

    这个工具我们前面文章也介绍过。通过attach到特定的进程中,可以查看当前进程的VM参数,内在信息等,其中也饮食我们关心的类路径信息。


3. JvisualVm

这个工具中也可以罗列出类路径信息,和Jconsole的类似,这里不多介绍。该工具可以添加插件,包含更多的功能,感兴趣的朋友可以自行探索。

当然,通过linux的ps命令也可以,这些java工具之外的其它方式不再罗列。

Tomcat中的classpath

前面说了这么多,但是在Tomcat中,对于classpath却并不是这样使用的。因为对于Tomcat这一类的Java应用服务器,并不能以-cp这种形式来定义类路径来表现依赖,这样可能会影响其它应用内的类依赖。类加载器实现的应用间依赖隔离请阅读前面的文章。(Tomcat类加载器以及应用间class隔离与共享)

了解这一点,我们来从Tomcat的启动脚本入手,按图索骥,来详细了解其对于classpath的配置。

一般对于Tomcat的启动,,都是通过startup这个脚本进行的,这个脚本内,主要的是执行这样一行命令

exec "$PRGDIR"/"$EXECUTABLE" start "$@"

其中EXECUTABLE指向了catalina.sh/catalina.bat这个脚本。重要的JVM参数以及类路径配置都在那里。

而在脚本里,对于classpath的配置,仅仅是把启动Bootstrap类需要的tomcat-juli.jar和bootstrap.jar。


那后面依赖的那些jar文件,像catalina.jar,Servlet和JSP规范的那些文件,是什么时候添加到classpath的呢?

我们前面关于Web应用的隔离和共享实现(Tomcat类加载器以及应用间class隔离与共享)一文中,提到过Tomcat通过使用common classLoader来实现类共享。这里我们来简单看下创建classLoader的过程。

其中common classLoader的创建,是把catalina.properties里对于commons.loader的配置,添加到其类路径中。

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

后面创建classLoader时,根据repositories展开,获取具体目录下的jar文件,以URL这种形式进行使用。


而ClassLoaderFactory创建的,是以这个set转换array生成的一个URLCalssLoader。

return AccessController.doPrivileged(
        new PrivilegedAction<URLClassLoader>() {
            @Override
            public URLClassLoader run() {
                if (parent == null)
                    return new URLClassLoader(array);
                else
                    return new URLClassLoader(array, parent);
            }
        });

我们再近一步,来看URLClassLoader的构造过程

public URLClassLoader(URL[] urls) {
    super();
    // this is to make the stack depth consistent with 1.1
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkCreateClassLoader();
    }
    ucp = new URLClassPath(urls);
    this.acc = AccessController.getContext();
}

这里我们得到的那些Jar文件的具体URL,就传过来,生成了URLClassPath。

/* The search path for classes and resources */
private final URLClassPath ucp;

这些内容,最终被以Stack的形式,保存了下来

private void push(URL[] var1) {
    Stack var2 = this.urls;
    synchronized(this.urls) {
        for(int var3 = var1.length - 1; var3 >= 0; --var3) {
            this.urls.push(var1[var3]);
        }

    }
}

这里保存下来的,就是后面加载类时要用到的内容。不管是否双亲委托,查找类基本都是先查看是否已经加载,未加载的再去类路径里查找。而在类路径查找时,就会从ucp中直接查了。

protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

相关阅读:

你了解JMX在Tomcat的应用吗?

Servlet到底是单例还是多例你了解吗?

深度揭秘乱码问题背后的原因及解决方式

类加载器与类冲突

关注Tomcat那些事儿,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值