要通过Web服务器加载war
包,我们首先要了解JVM的ClassLoader(类加载器)的机制。
在Java中,所有的类,都是由ClassLoader加载到JVM中执行的,但JVM中不止一种ClassLoader。写个简单的程序就可以测试:
public class Main {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); // null
System.out.println(DataSource.class.getClassLoader()); // PlatformClassLoader
System.out.println(Main.class.getClassLoader()); // AppClassLoader
}
}
对于Java核心类,如java.lang.String
,返回null
表示使用的是JVM内部的启动类加载器(BootClassLoader
),对于非核心的JDK类,如javax.sql.DataSource
,使用的是PlatformClassLoader
,对于用户编写的类,如Main
,使用的是AppClassLoader
。
我们通常说的ClassPath机制,即JVM应该在哪些目录和哪些jar包里去找Class,实际上说的是AppClassLoader
使用的ClassPath,这3个ClassLoader按优先级排序如下:
用AppClassLoader加载一个Class时,它首先会委托父级ClassLoader尝试加载,如果加载失败,才尝试自己加载,这就是JVM的ClassLoader使用的双亲委派模型,它是为了防止用AppClassLoader加载用户自己编写的java.lang.String
导致破坏JDK的核心类。
因此,对于一个Class来说,它始终关联着一个加载它自己的ClassLoader:
┌───────────────────────────────┐
┌───────────────────┐ │java.lang.String │
│ BootClassLoader │◀─ ─ ─│java.util.List │
└───────────────────┘ │... │
▲ └───────────────────────────────┘
│ ┌───────────────────────────────┐
┌───────────────────┐ │javax.sql.DataSource │
│PlatformClassLoader│◀─ ─ ─│javax.transaction.xa.XAResource│
└───────────────────┘ │... │
▲ └───────────────────────────────┘
│ ┌───────────────────────────────┐
┌───────────────────┐ │com.example.Main │
│ AppClassLoader │◀─ ─ ─│org.slf4j.Logger │
└───────────────────┘ │... │
└───────────────────────────────┘
现在,假设我们完成了Jerrymouse服务器的开发,那么最后得到的就是jerrymouse.jar
这样的jar包,如果要运行一个hello-webapp.war
,我们期待的命令行如下:
$ java -jar jerrymouse.jar --war hello-webapp.war
上述命令行的classpath实际上是jerrymouse.jar
,服务器的类均可以被JVM的AppClassLoader
加载,但是,AppClassLoader
无法加载hello-webapp.war
在/WEB-INF/classes
存放的.class
文件,也无法加载在/WEB-INF/lib
存放的jar文件,原因是它们均不在classpath中,且运行期无法修改classpath。
因此,我们必须自己编写ClassLoader,才能加载到hello-webapp.war
里的.class
文件和jar
包。
为了加载war
包里的.class
文件和jar
包,我们定义一个WebAppClassLoader
。直接从ClassLoader
继承不是不可以,但是要自己编写的代码太多。ClassLoader看起来很复杂,实际上就是想办法以任何方式拿到.class
文件的用byte[]
表示的内容,然后用ClassLoader
的defineClass()
获得JVM加载后的Class
实例。大多数ClassLoader都是基于文件的加载,因此,JDK提供了一个URLClassLoader
方便编写从文件加载的ClassLoader:
public class WebAppClassLoader extends URLClassLoader {
public WebAppClassLoader(Path classPath, Path libPath) throws IOException {
super("WebAppClassLoader", createUrls(classPath, libPath), ClassLoader.getSystemClassLoader());
}
// 返回一组URL用于搜索class:
static URL[] createUrls(Path classPath, Path libPath) throws IOException {
List<URL> urls = new ArrayList<>();
urls.add(toDirURL(classPath));
Files.list(libPath).filter(p -> p.toString().endsWith(".jar")).sorted().forEach(p -> {
urls.add(toJarURL(p));
});
return urls.toArray(URL[]::new);
}
static URL toDirURL(Path p) {
// 将目录转换为URL:
...
}
static URL toJarURL(Path p) {
// 将jar包转换为URL:
...
}
}
只要传入正确的目录和一组jar包,WebAppClassLoader
就可以加载到对应的.class
文件。
下一步是修改启动流程,先解析命令行参数--war
拿到war
包的路径,然后解压到临时目录,获取到/tmp/xxx/WEB-INF/classes
路径以及/tmp/xxx/WEB-INF/lib
路径,就可以构造WebAppClassLoader
了:
Path classesPath = ...
Path libPath = ...
ClassLoader classLoader = new WebAppClassLoader(classesPath, libPath);
接下来,需要获取到所有的Servlet
、Filter
和Listener
组件,因此需要在WebAppClassLoader
的范围内扫描所有.class
文件:
Set<Class<?>> classSet = ... // 扫描获得所有Class
修改HttpConnector
,传入ClassLoader
和扫描的Class,就可以把所有Servlet
、Filter
和Listener
添加到ServletContext
中。这样,我们就把写死的Servlet组件从服务器中移除掉,并实现了从外部war包动态加载Servlet组件。
在HttpConnector
中,我们还需要对handler()
方法进行改进,正确设置线程的ContextClassLoader(上下文类加载器):
public void handle(HttpExchange exchange) throws IOException {
var adapter = new HttpExchangeAdapter(exchange);
var response = new HttpServletResponseImpl(this.config, adapter);
var request = new HttpServletRequestImpl(this.config, this.servletContext, adapter, response);
try {
// 将线程的上下文类加载器设置为WebAppClassLoader:
Thread.currentThread().setContextClassLoader(this.classLoader);
this.servletContext.process(request, response);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 恢复默认的线程的上下文类加载器:
Thread.currentThread().setContextClassLoader(null);
response.cleanup();
}
}
为什么需要设置线程的ContextClassLoader?执行handle()
方法的线程是由线程池提供的,线程池是HttpConnector
创建的,因此,handle()
方法内部加载的任何类都是由AppClassLoader
加载的,而我们希望加载的类是由WebAppClassLoader
从解压的war
包中加载,此时,就需要设置线程的上下文类加载器。
举例说明:
当我们在一个方法中调用Class.forName()
时:
Object createInstance(String className) {
Class<?> clazz = Class.forName(className);
return clazz.newInstance();
}
正常情况下,将由AppClassLoader
负责查找Class
,显然是找不到war包解压后存放在classes
和lib
目录里的类,只有我们自己写的WebAppClassLoader
才能找到,因此,必须设置正确的线程上下文类加载器:
Object createInstance(String className) {
Thread.currentThread().setContextClassLoader(this.classLoader);
Class<?> clazz = Class.forName(className);
Thread.currentThread().setContextClassLoader(null);
return clazz.newInstance();
}
最后,完善所有接口的实现类,我们就成功开发了一个迷你版的Tomcat服务器!
开发Web服务器时,需要编写自定义的ClassLoader,才能从war包中加载.class
文件;
处理Servlet请求的线程必须正确设置ContextClassLoader。