一、背景
几个Web项目需要打包到一个发布包中,问题是War包都各自包含了自己的WEB-INF/lib,其中不少依赖Jar有重复。所以希望把War包的所有的lib都放到外部的一个公共目录减小总工程大小。但放在一个目录,以目录方式加载到全局Classpath就会产生类库冲突问题,如:web1使用spring2,web2使用spring3。
二、分析
一般Jar包中都包含一个META-INF/MANIFEST.MF文件。在这个配置文件中可以指定一个Class-Path属性,再执行java -jar xxx.jar时,Java会以这个Class-Path属性中定义为准。
War包中也有MANIFEST.MF文件,当然也可以添加Class-Path属性(Maven)。但问题是这个属性从Java规范上讲并没有约定,所以应用服务器一般都不识别。
我们需要解决的是类路径加载的问题。还好Tomcat上下文配置文件的Context标签中有Loader标签,允许我们重写WebappLoader。
三、开发
1、Tomcat应用上下文XML配置样例
在Tomcat应用上下文配置文件中,定义Web工程的Loader指定到自定义WebappLoader。
2、ManifestClasspathWebappLoader类源码
继承WebappLoader,override startInternal方法。
package org.noahx.tomcat;
import org.apache.catalina.Container;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappLoader;
import java.io.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* 从War中的MANIFEST.MF获得类路径并进行额外加载
* User: noah
* Date: 10/29/13
* Time: 1:20 AM
* To change this template use File | Settings | File Templates.
*/
public class ManifestClasspathWebappLoader extends WebappLoader {
public ManifestClasspathWebappLoader() {
super();
}
public ManifestClasspathWebappLoader(ClassLoader parent) {
super(parent);
}
@Override
protected void startInternal() throws LifecycleException {
final Container container = getContainer();
if (container instanceof StandardContext) {
final String docBase = ((StandardContext) container).getDocBase();
File baseFile = new File(docBase);
if (baseFile.exists() && baseFile.canRead()) { //是否可读
if (baseFile.isDirectory()) { //目录
final File manifestFile = new File(baseFile, "META-INF/MANIFEST.MF");
if (manifestFile.exists() && manifestFile.canRead() && manifestFile.isFile()) { //MANIFEST.MF文件可读
System.out.println("found MANIFEST.MF" + manifestFile);
try {
FileInputStream fileInputStream = new FileInputStream(manifestFile);
setClasspaths(baseFile, fileInputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
} else if (baseFile.isFile()) { //文件(war)
//目前没有实现war方式的解析
}
}
}
super.startInternal();
}
/**
* 设置MANIFEST.MF流中的类路径
*
* @param baseFile
* @param inputStream
*/
private void setClasspaths(File baseFile, InputStream inputStream) {
String classpaths[] = null;
try {
final Manifest manifest = new Manifest(inputStream);
final Attributes attributes = manifest.getMainAttributes();
String classpathValue = attributes.getValue("Class-Path");
if (classpathValue != null) { //不为null说明发现Class-Path
classpathValue = classpathValue.replaceAll("[\r\n]+$", ""); //移除换行
classpaths = classpathValue.split("\\s+"); //拆分类路径字符串
}
} catch (IOException e) {
e.printStackTrace();
}
if (classpaths != null) { //如果发现类路径则设置类路径
for (String classpath : classpaths) {
addRepository(new File(baseFile.getParent(), classpath).toURI().toString()); //转换相对路径为实际路径并转换为URI
}
System.out.println(baseFile.getName() + " append " + classpaths.length + " classpaths.");
}
}
}
只实现了目录方式的Web部署,大家可以按需要扩展War文件方式的。
addRepository方法就是在为Tomcat启动该Web工程提供新扩展的类路径。
*该类需要以Jar包方式放在Tomcat的lib目录(该lib中包含tomcat的Jar,如:catalina.jar等)中。
3、Web工程添加Class-Path属性的方法(Maven)
maven-war-plugin
true
../lib/
false
War包中MANIFEST.MF文件,实际效果文件如下:
4、Tomcat后台控制台输出
found MANIFEST.MF/nautilus/xxxxxxx/apps/xxx/META-INF/MANIFEST.MF
xxx append 90 classpaths.
四、总结
所有Web工程的Jar包都放在外部的公共目录,减小了空间占用。虽然在同一个目录但相互之间并不会产生任何影响,Tomcat会按War包的Class-Path属性一个一个文件进行精确加载类路径(非目录)。