在很多Apache Tomcat用户论坛,一个问题经常被提出,那就是如何配置Tomcat的classpath,使得一个web应用程序能够找到类或者jar文件,从而可以正常工作。
就像许多困扰Tomcat新用户的问题一样,这个问题也很容易解决。
在这篇文章中,我们将会介绍Tomcat是如何产生和利用classpath的,然后一个一个解决大多数常见的与classpath相关的问题。
为什么Classpaths给Tomcat用户带来了麻烦
一个classpath就是一个参数,来告诉java虚拟机在哪里可以找到类和包去运行一个程序。classpath总是在程序源码外设置的,将其同程序分开可以允许java代码以一种抽象的方式来引用类和包,允许程序可以在任何系统上被配置。
为什么那些很有经验的java用户,他们已经非常清楚classpath是什么了,但是在Tomcat运行程序的时候,还是会遇到这样那样的问题呢?
可能有以下三个原因:
1、Tomcat处理classpaths的方式与其他java程序是不同的
2、Tomcat的版本不同,其处理classpaths的方式也可能不同
3、Tomcat的文档和默认的配置要求以一种特定的方式来完成某些事情。如果你不遵循这种方式,那么你就会陷入麻烦之中。关于如何解决常见的classpath问题,没有信息可以提供,比如外部依赖,共享依赖或者多版本的相同依赖。
Tomcat的Classpath如何不同于标准的Classpath
Apache Tomcat目的是尽可能的独立,直观和自动化,为了有效的管理,标准化web应用程序的配置和部署过程,为了安全和命名控件的考虑,限制对不同库的访问。
这就是为什么不使用java classpath环境变量的原因了,java的classpath是声明依赖类库默认的地方,Tomcat的start脚本忽略了这个变量,在创建Tomcat系统classloader的时候产生了自己的classpaths。
想要理解Tomcat如何处理classpath的,看看以下Tomcat6的启动过程:
1、java虚拟机的bootstrap loader加载java的核心类库。java虚拟机使用JAVA_HOME环境变量来定位核心库的位置。
2、Startup.sh,使用start参数调用Catalina.sh,重写系统的classpath并加载bootstrap.jar和tomcat-juli.jar。这些资源仅对Tomcat可见。
3、为每一个部署的Context创建Class loader,加载位于每个web应用程序WEB-INF/classes和WEB-INF/lib目录下的所有类和jar文件。每个web应用程序仅仅可见自己目录下的资源。
4、Common class loader加载位于$CATALINA_HOME/lib目录下的所有类和jar文件,这些资源对所有应用程序和Tomcat可见。
Tomcat的类加载机制是如何在不同版本之间变化的
之前的Tomcat版本,其类加载机制有一些不同
Tomcat 4.x以及更早的版本,一个server loader负责加载Catalina类,现在由commons loader负责了。
Tomcat 5.x,一个shared loader负责加载在应用程序间共享的类,这些类位于$CATALINA_HOME/shared/lib。在Tomcat6中,这种方式被取消了。
Tomcat 5.x也包括了一个Catalina loader,加载所有的Catalina组件,现在也被Common loader取代了。
当你不能按照Tomcat要求的方式做事的时候,怎么办
如果你使用Tomcat文档推荐的方式做事,你不应该有关于classpath的问题。
你的wars包含了所有库和包的复本,你没有任何理由去在多个应用程序间共享一个jar文件,你不需要调用任何外在的资源,你也将不会遇到复杂的情况,例如一个web应用程序运行的时候需要一个jar文件的多个版本。
但是如果你确实不能按照推荐的方式来做的时候,一个文件可以解决你所有的问题:catalina.properties。
使用catalina.properties来配置Tomcat Classpath
对于那些不想使用默认来加载方式的用户来说,幸运的是,Tomcat的classpath选项不是硬编码的,它们是从$CATALINA_HOME/conf/catalina.properties文件中读取的。
这个文件包含了除bootstrap和system loader之外的所有其他的loaders,检查这个文件,你会有一些新发现:
1、Server以及Shared loader还没有被删除,如果它们的属性没有定义,Commons loader负责处理。
2、被各种loaders加载的类和jar文件不会被自动加载,它们只是用一个简单的通配符语法指定为一组。
3、这里没有说你不能指定一个额外的仓库,事实上就是说你是可以的。
server loader不应该改动,但是shared loader还是有许多有用的应用。(shared loader将会在启动过程的最后阶段加载它的类,在Commons loader之后)。
现在让我们看看一些常见的问题以及如何解决。
问题、解决方案和最佳实践
问题:我的应用程序依赖一个外部的仓库,我不能引用它。
让Tomcat知道一个外部的仓库,在catalina.properties文件的shared loader位置,使用正确的语法,声明这个仓库。语法基于你要配置的文件或仓库的类型:
1、增加一个文件夹作为类仓库,使用“path/to/foldername”
2、增加一个文件夹下的所有jar文件作为类仓库,使用"path/to/foldername/*.jar"
3、增加单个jar文件作为类仓库,使用"file://path/to/foldername/jarname.jar"
4、调用环境变量,使用${}格式,例如${VARIABLE_NAME}
5、声明多个资源,用逗号分隔开
6、所有的路径相对于CATALINA_BASE或CATALINA_HOME,或者是绝对路径
问题:我想多个应用程序共享一个jar文件,这个jar文件在Tomcat里面。
除了一些常见的第三方库(比如JDBC drivers),最好不要在$CATALINA_HOME/lib目录下包含额外的库,即使这样在一些情况下是可行的。应该重新创建比如/shared/lib和/shared/classes目录,然后在catalina.properties配置shared.loader属性:
"shared/classes,shared/lib/*.jar"
问题:除了另一个框架,我在一个应用中使用了一个嵌入式Tomcat server,当我访问框架组件的时候遇到classpath errors。
这个问题好像超出了这篇文章的范畴,但是作为一个常见的classpath相关的问题,这里对如何引起你的错误作一个简单的介绍。
当嵌入到包含另外核心框架(Wicket或者Spring)的应用中时,Tomcat将在启动框架的时候,使用System classloader加载核心类,而不是从应用的WEB-INF/lib目录下加载。
java的类加载是懒加载,就是说请求某个类的第一个加载器拥有这个类剩下的生命周期。如果System classloader,它的类对web应用是不可见的,首先加载了框架相关的类,java虚拟机将会阻止来的其他实例被创建,这样就引起了classpath错误。
解决这个问题的一种方式就是增加一个自定义的bootstrap classloader,使得这个classloader加载合适的库,然后对程序剩下部分的启动正常对待。
问题:我使用一个标准的应用程序,程序的war包含了依赖的所有包,但是我仍然遇到类定义错误。
这个问题可能是由许多事情引起的,包括编译或部署过程不是很正确,但是最有可能是web应用程序的目录结构不对造成的。
java命名转换要求类名映射到存放这个类的目录结构。例如,一个类com.mycompany.mygreat.class需要被存放到目录WEB-INF/classes/com/mycompany/。
经常代码中丢失一个点号就会引起classpath相关的问题。
问题:我的web应用程序的不同模块需要使用同一个jar包的两个不同版本。
这种情况常常出现在一个应用程序中使用多个web框架,这些web框架依赖一个库的不同版本。
有几种解决方案,但是它们都和classpath不相关。
我们在这里说这个问题,是因为一些用户试图在框架的jar文件中的Manifest文件中指定依赖的库的不同版本的classpath,来解决这个问题。
这种方式在一些web应用服务器中是支持的,所以一些用户想要在Tomcat中也使用这种方式。但是在Manifest文件中指定classpath在Tomcat中是不支持的,这也不是Servlet说明的一部分。
有以下四种方式来解决这个问题:
1、你可以更新框架的版本,如果这里能够使得与其他框架依赖相同的版本。
2、你可以创建两个或更多的自定义classloaders,每个jar文件一个,在WEB/INF/context.xml文件中配置,创建你所需要的两个不同版本的类的实例。
3、你可以使用jarjar工具将框架和它依赖的库打包成一个jar文件,那么它们会一起被加载。
4、如果你发现你每隔一天就要处理这种情况,你应该考虑使用OSGI框架,这个框架有许多方法专门就是用来处理这种一个类的多个版本需要运行在同一个java虚拟机里的情况的。
最佳实践
1、避免在Tomcat里使用Commons loader加载不属于Tomcat标准发布的库和包,这可能会引起兼容错误。如果你需要在多个应用程序间共享一个库或包,创建shared/lib和shared/classes目录,然后配置到catalina.properties文件的Shared loader。
2、以上规则的一个例外就是常用的第三方共享库,例如JDBC driver。这些应该放到$CATALINA_HOME/lib目录下。
3、可能的话,尽可能按照Tomcat的开发者推荐的方式使用Tomcat,如果你发现你不得不频繁配置classpath,你可能需要重新考虑你的开发过程了。
Reference:http://www.mulesoft.com/tomcat-classpath。