跟我一起动手实现Tomcat(二):实现简单的Servlet容器

最近笔者读了《深入剖析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这个接口。

    项目加个依赖:

        javax.servlet
        javax.servlet-api
        3.0.1
    
    

    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文件夹下,打开浏览器走一波:


搞定!

不对…其实上面的设计是有很严重的缺陷的

加强Request、Response安全性

  • 缺陷在哪里

细心的哥们肯定发现了:我们在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类图关系如下:


代码也很简单都是调用内部request对象的相应方法:

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

稀土掘金责编内容来自:稀土掘金 (源链) | 更多关于

阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合编程 » 跟我一起动手实现Tomcat(二):实现简单的Servlet容器

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录