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

前言

章节循序渐进的讲解了tomcat的原理,在接下来的章节中,tomcat都是基于上一章新增功能并完善,
到最后形成一个简易版tomcat的完成品。所以有兴趣的同学请按顺序阅读,本文为记录第二章的知识点
以及源码实现(造轮子)。
复制代码

内容回顾

跟我一起动手实现Tomcat(一):实现静态Web服务器

上一章我们实现了简单的静态资源web服务器,能够读取到用户自定义的HTML/css/js/图片并显示到浏览器以及404页面的展示等。

本章内容

本章会实现简单的Servlet容器,能够根据用户请求URI调用对应的Servlet的service()方法并执行,init()/destory()方法和HttpServletRequest/HttpServletResponse里面的大部分方法本章仍未实现,会在下面的几章逐步完善。

开始之前

  • javax.servlet.Servlet

    咱们web开发的同学都知道,刚学习web开发的时候都是先实现这个Servlet接口去自定义自己的
    Servlet类的,那么在这里简单的回顾一下Servlet这个接口。
    复制代码

    项目加个依赖:

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.0.1</version>
    </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回显的内容就看不到了

)
ServletProcess大致调用流程:

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接口的方法我们仍未实现
- 其他未实现的功能
复制代码

在下一个章节我们会实现Request解析Parameter、HTTPHeader、Cookie等参数并重构架构模式:

跟我一起动手实现Tomcat(三):解析Request请求参数、请求头、cookie

PS:本章源码已上传github SimpleTomcat

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值