最近笔者读了《深入剖析tomcat》这本书(原作:《how tomcat works》),发现该书简单易读,每个章节
循序渐进的讲解了tomcat的原理,在接下来的章节中,tomcat都是基于上一章新增功能并完善,到最后形成
一个简易版tomcat的完成品。所以有兴趣的同学请按顺序阅读,本文为记录第二章的知识点以及源码实现
(造轮子)。
点我阅读上一章内容
上一章我们实现了简单的静态资源web服务器,能够读取到用户自定义的HTML/css/js/图片并显示到浏览器以及404页面的展示等。
本章会实现简单的Servlet容器,能够根据用户请求URI调用对应的Servlet的service()方法并执行,init()/destory()方法和HttpServletRequest/HttpServletResponse里面的大部分方法本章仍未实现,会在下面的几章逐步完善。
javax.servlet.Servlet
咱们web开发的同学都知道,刚学习web开发的时候都是先实现这个Servlet接口去自定义自己的
Servlet类的,那么在这里简单的回顾一下Servlet这个接口。
项目加个依赖:
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.0.1version>
dependency>
Servlet接口方法一览(具体方法干嘛的大家应该都懂了,就不介绍了):
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}
如何实现
在这里基于上一章的代码,只要用户输入127.0.0.1:8080/servlet/{servletName},我们就将这个URI提取出具体的servlet名字,使用java.net包下的URLClassLoader将这个Servlet类加载并实例化,然后调用它的service()方法,一次Servlet调用就这样完成啦,是不是很简单呢,来让我们看看代码怎么去实现!!!
1. 实现相应的接口
我们先把上个章节的Request、Response分别实现ServletRequest、ServletResponse接口(这是Servlet规范),具体实现的方法咱们什么都不做,等以后再完善。
public class Request implements ServletRequest {
...省略N个方法
}
public class Response implements ServletResponse {
/*Response只实现这个方法,把我们socket的outputStream封装成一个PrintWriter*/
@Override
public PrintWriter getWriter() throws IOException {
PrintWriter writer = new PrintWriter(outputStream,true);
return writer;
}
}
2. 不同资源使用不同的执行器
我们的tomcat准备要支持servlet调用了,那么servlet和普通静态资源不一样,那么我们在代码层面应该将他们隔离开来,以方便日后的扩展,在这里我们实现以下两个执行器:
- ServletProcess 专门执行Servlet的执行器
- StaticResourceProcess 执行静态资源的执行器
那么我们看看我们现在一个请求的执行流程:
好吧其实大家可以看到,跟以前变化也不是很大,只是多了个if判断,然后把相应的执行过程丢到执行器里面去执行而已~那我们来看看对应的实现:
HttpServer
大家应该还记得HttpServer吧,是我们启动程序的主入口以及ServerSocket监听实现。
它的改动不大,只是加了个if判断:
public static void main(String[] args) {
ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
....
//解析用户的请求
Request request = new Request();
request.setRequestStream(inputStream);
request.parseRequest();
//生成相应的响应
Response response = new Response(outputStream, request);
//根据URI调用不同的处理器处理请求
if (request.getUri().startsWith("/servlet/")) {
new ServletProcess().process(request, response);
} else {
new StaticResourceProcess().process(request, response);
}
...
}
StaticResourceProcess
StaticResourceProcess也没干啥,只是调用了上个章节读取静态资源的方法
public class StaticResourceProcess {
public void process(Request request, Response response) throws IOException {
response.accessStaticResources();
}
}
ServletProcess
ServletProcess持有了一个URLClassLoader静态变量,专门用来加载Servlet:
private static final URLClassLoader URL_CLASS_LOADER;
static {
/*定位到我们的webroot/servlet/文件夹*/
URL servletClassPath = new File(HttpServer.WEB_ROOT, "servlet").toURI().toURL();
//初始化classloader
URL_CLASS_LOADER = new URLClassLoader(new URL[]{servletClassPath});
}
现在我们知道以/servlet/开头的URI请求是需要调用Servlet资源的,那么我们怎么提取Servlet的名字并初始化呢?先来看看一个URI:
/servlet/TestServlet
好像也不是很难提取,直接用String的lastIndexOf和substring方法就可以搞定啦:
uri = uri.substring(uri.lastIndexOf("/") + 1);
前面的难题也都解决了,那么我们看看process是怎么执行的:
public void process(Request request, Response response) throws IOException {
//就是上面的那个字符串截取方法
String servletName = this.parseServletName(request.getUri());
//使用URLClassLoader加载这个Servlet并实例化
Class servletClass = = URL_CLASS_LOADER.loadClass(servletName);
Servlet servlet = (Servlet) servletClass.newInstance();
response.getWriter().println(new String(response.responseToByte(HttpStatusEnum.OK)));
//调用servlet的service方法
servlet.service(request,response);
}
大家可能不太理解倒数第二行的代码,它就是调用了Response.PrintWriter(我们刚才上面用socket的outputStream封装的)对象向浏览器输出了一个响应头(不这么做傲娇的chrome会认为这个响应是无效的,servlet回显的内容就看不到了)
3.准备一个自定义Servlet
我们Servlet容器也算开发完成了,我们搞一个servlet做做实验吧~
public class TestServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("Start invoke TestServlet ... ");
res.getWriter().println("Hello Servlet!");
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}
它只是在控制台输出一个记录以及向浏览器回显一句话(是不是觉得不能处理参数很无聊,下面几章我们就会实现它),把这个类编译成class文件,丢到我们resource/webroot/servlet文件夹下,打开浏览器走一波:
搞定!
不对…其实上面的设计是有很严重的缺陷的
细心的哥们肯定发现了:我们在ServletProcess调用用户自定义的servlet的时候,是直接将Request/Response作为参数传入用户的service方法中(因为我们的reuqest、response实现了ServletRequest、ServletResponse接口),那么如果我们的这个tomcat拿去发布给其他人使用的时候,阅读过我们的tomcat源码的人的servlet就可以这样写:
public class TestServlet {
public void service(HttpServletRequest request,HttpServletResponse response){
((Request)request).parseRequest("");
((Response)response).accessStaticResources();
}
}
上面那两个方法我们设计时是提供我们process或者其他时候使用的(所以方法不能设置为private),并不是提供给用户调用的,这就破坏了封装性了!!
解决方案
有看过或者阅读过Tomcat源码的时候,发现Tomcat已经用了一种设计模式去解决这个缺陷了,就是外观设计模式(门面设计模式),具体设计模式大家可以去搜索了解一下,在这里我们也引用这种设计模式处理这个缺陷,UML类图关系如下:
public class RequestFacade implements ServletRequest{
private Request request;
@Override
public Object getAttribute(String name) {
return request.getAttribute(name);
}
其他实现的方法也类似...
}
在ServletProcess方法调用servlet时我们用Facade类包装一下:
...
Servlet servlet = (Servlet) servletClass.newInstance();
servlet.service(new RequestFacade(request), new ResponseFacade(response));
...
就此大功告成!
使用者顶多只能将ServletRequest/ServletResponse向下转型为RequestFacade/ResponseFacade
但是我们没提供getReuqest()/getResponse()方法,所以它能调用的方法还是相应ServletRequest、
ServletResponse接口定义的方法,这样我们内部的方法就不会被用户调用到啦~
到这里,咱们的Tomcat 2.0 web服务器就已经开发完成啦(滑稽脸),已经可以实现简单的自定义Servlet调用,但是很多功能仍未完善:
- 每一次请求就new一次Servlet,Servlet应该在初始化项目时就应该初始化,是单例的。
- 并未遵循Servlet规范实现相应的生命周期,例如init()/destory()方法我们均未调用。
- ServletRequest/ServletResponse接口的方法我们仍未实现
- 其他未实现的功能
在下一个章节我们会实现类似Tomcat的连接器(Connector)功能,并且优化我们解析HTTP协议的方法,敬请期待!
PS:本章源码已上传github SimpleTomcat