每个Java开发者,从开始学习Java的那一天起,就认识了classpath。也许你只是不经意的输入了一个java HelloWorld 但这个时候,你已经无意中使用到了classpath。
说了半天,什么是classpath,有什么用呢?
概念
我们都知道,在JAVA的世界里,两个变更至关重要:path、classpath 其中path指定了我们所使用的javac、java这些可招待程序的发行版具体路径,而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默认饮食的小工具,可以直接使用。
jps
这个命令我们在前面的文章里曾经介绍过。(你可能不知道的几个java小工具)通过
jps -lv可以列出当前运行的所有Java进程,以及其详细的参数信息,这样我们的配置的classpath内容就罗列出来了。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; }
相关阅读:
关注Tomcat那些事儿,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

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





