深入剖析Tomcat(二) 实现一个简单的Servlet容器

现在开始《深入剖析Tomcat》第二章的内容,第一章中,我们编码实现了一个能正常接收HTTP请求并返回静态资源的Web容器,这一章开始引入Servlet的概念,使我们的服务能根据请求动态返回内容。

Servlet是什么?

这是首先要弄清楚的一个东西,有篇文章对它的历史由来介绍的很好【Servlet的历史与规范】,推荐阅读它的第一节:Servlet历史。

如果你是一个Java开发者,那么你经常写的Controller就可以视为一个Servlet。Servlet是处理某个特定HTTP请求的组件程序,它不能独立工作,由Servlet容器统一管理众多Servlet,Servlet容器负责将HTTP请求分发给指定的Servlet。

如上图所示,Tomcat大概分为连接器和Servlet容器两大块,连接器负责接收HTTP请求,并封装成Request与Response对象,然后将处理过程交给Servlet容器,Servlet容器会判断这个请求该交给哪个Servlet来处理,Servlet处理完后给客户端反馈。

Servlet规范是Sun公司制定的一套技术标准,包含与Web应用相关的一系列接口,是Web应用实现方式的宏观解决方案。而具体的Servlet容器则根据规范提供标准的实现。Tomcat,Jetty 等都是Servlet容器,它们都遵循Servlet规范,也就是说它们都是基于Servlet的标准接口来实现的自定义逻辑。

Servlet规范内容很多,这里仅提一下本章涉及到的知识,其他知识待用到时再补充。下面介绍几个规范中的接口

javax.servlet.Servlet

实现了 javax.servlet.Servlet 接口的类,就是一个Servlet。

package javax.servlet;

import java.io.IOException;
/**
 * 定义所有servlet必须实现的方法。
 * servlet是在Web服务器中运行的小Java程序。
 * servlet通常通过HTTP(超文本传输协议)接收和响应来自Web客户机的请求。
 * 要实现这个接口,可以编写一个通用的servlet,例如 继承javax.servlet.GenericServlet,也可以编写一个HTTP servlet通过继承javax.servlet.http.HttpServlet。
 * 这个接口定义了初始化servlet、处理请求和从容器中删除servlet的方法。这些方法被称为生命周期方法,按以下顺序调用:
 * 1.首先构造servlet,然后使用init方法进行初始化。
 * 2.service方法处理客户端的所有请求。
 * 3.使用destroy方法销毁servlet,然后进行垃圾收集并最终完成销毁过程。
 * 除了生命周期方法之外,这个接口还提供了getServletConfig方法(servlet可以使用该方法获取任何启动信息)
 * 和getServletInfo方法(允许servlet返回关于自身的基本信息,例如作者、版本和版权)。
 */
public interface Servlet {
    /**
     * 由servlet容器调用,以指示servlet正在被放入服务中。
     * servlet容器在实例化servlet之后只调用init方法一次。
     * init方法必须成功完成,servlet才能接收请求。
     * 如果init方法发生下列情况,servlet容器就不能将servlet放入容器
     * 1.抛出异常 ServletException
     * 2.没有在Web服务器定义的时间段内返回
     */
    void init(ServletConfig var1) throws ServletException;
    
    /**
     * 返回一个ServletConfig对象,其中包含此servlet的初始化和启动参数。返回的ServletConfig对象是传递给init方法的对象。 
     * 这个接口的实现负责存储ServletConfig对象,以便这个方法可以返回它。实现这个接口的GenericServlet类已经做到了这一点。
     */
    ServletConfig getServletConfig();

    /**
     * 由servlet容器调用,以允许servlet响应请求。
     * 此方法仅在servlet的init()方法成功完成后调用。
     * 响应的状态码应该始终为抛出或发送错误的servlet设置。
     * servlet通常运行在多线程servlet容器中,可以并发处理多个请求。开发人员必须注意同步访问任何共享资源,如文件、网络连接
     * 以及servlet的类和实例变量。有关Java中多线程编程的更多信息,请参见Java多线程编程教程。
     */
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    /**
     * 返回有关servlet的信息,如作者、版本和版权。 
     * 此方法返回的字符串应该是纯文本,而不是任何类型的标记(如HTML、XML等)。
     */
    String getServletInfo();
    
    /**
     * 由servlet容器调用,以指示servlet正在退出服务。此方法仅在servlet服务方法中的所有线程都退出或超时时间过后才调用。
     * 在servlet容器调用此方法之后,它将不会在此servlet上再次调用该服务方法。 
     * 此方法为servlet提供了一个机会来清理任何被占用的资源(例如,内存、文件句柄、线程),并确保任何持久状态都与servlet在内存中的当前状态同步。
     */
    void destroy();
}

javax.servlet.ServletConfig

package javax.servlet;

import java.util.Enumeration;

/**
 * 一个servlet配置对象,由servlet容器使用,在初始化期间向servlet传递信息
 */
public interface ServletConfig {

    /**
     * 返回此servlet实例的名称。该名称可以通过服务器管理提供,在web应用程序部署描述符中分配,或者对于未注册(因此未命名)的servlet实例,它将是servlet的类名。
     */
    public String getServletName();

    /**
     * 返回对调用者正在其中执行的ServletContext的引用。
     */
    public ServletContext getServletContext();

    /**
     * 返回一个包含初始化参数值的字符串,如果参数不存在则返回null。
     */
    public String getInitParameter(String name);

    /**
     * 返回servlet初始化参数的枚举,如果servlet没有初始化参数,则返回空枚举。
     */
    public Enumeration<String> getInitParameterNames();
}

javax.servlet.ServletContext

ServletContext接口定义的方法太多,就不在这里放代码了。

看一下这个接口的注释吧:

定义servlet用于与其servlet容器通信的一组方法,例如,获取文件的MIME类型、调度请求或写入日志文件。
每个Java虚拟机的每个“web应用程序”都有一个上下文。(“web应用程序”是安装在服务器URL命名空间(如/catalog)的特定子集下的servlet和内容的集合,可能通过.war文件安装。)
在部署描述符中标记为“分布式”的web应用程序的情况下,每个虚拟机将有一个上下文实例。在这种情况下,上下文不能用作共享全局信息的位置(因为信息不是真正的全局信息)。使用外部资源,比如数据库。
ServletContext对象包含在ServletConfig对象中,Web服务器在初始化servlet时提供ServletConfig对象。

下面列举这个接口的一些常用方法

  • String getContextPath():获取Web应用程序的上下文路径。
  • String getRealPath(String path):将指定的虚拟路径映射到实际路径。
  • RequestDispatcher getRequestDispatcher(String path):获取用于将请求转发到另一个资源的请求分派器。
  • InputStream getResourceAsStream(String path):获取位于Web应用程序上下文的指定路径的输入流。
  • URL getResource(String path):返回指定路径的URL,用于从ServletContext获取资源。
  • ServletContext getContext(String uripath):获取指定URI路径的ServletContext。
  • String getMimeType(String file):获取指定文件的MIME类型。
  • Set<String> getResourcePaths(String path):获取指定路径下的所有资源路径的集合。
  • String getInitParameter(String name):获取指定名称的初始化参数的值。
  • Enumeration<String> getInitParameterNames():获取所有初始化参数的名称的枚举。
  • void setAttribute(String name, Object object):在ServletContext中设置一个属性。
  • Object getAttribute(String name):获取指定名称的ServletContext属性。
  • void removeAttribute(String name):从ServletContext中移除指定名称的属性。
  • String getServerInfo():获取Servlet容器的名称和版本信息。
  • int getMajorVersion() 和 int getMinorVersion():获取Servlet API的主版本号和次版本号。

javax.servlet.ServletRequest 与 javax.servlet.ServletResponse

这两个类是javax.servlet.Servlet#service 方法需要的两个参数,也就是说 servlet 需要这两个参数来处理请求与返回结果,所有的HTTP请求打到Servlet容器时都需要封装成这两个对象。

ServletRequest 接口代表客户端请求,并提供了访问请求参数、请求头和其他请求信息的方法。以下是一些常用的方法:

  • String getParameter(String name):根据参数名称获取请求参数的值。
  • Enumeration<String> getParameterNames():获取所有请求参数名称的枚举。
  • String[] getParameterValues(String name):根据参数名称获取请求参数的值数组。
  • Map<String, String[]> getParameterMap():获取所有请求参数的映射。
  • String getHeader(String name):根据头部名称获取请求头的值。
  • Enumeration<String> getHeaderNames():获取所有请求头名称的枚举。
  • String getMethod():获取请求的 HTTP 方法,例如 GET、POST 等。
  • String getRemoteAddr():获取客户端的 IP 地址。
  • String getRemoteHost():获取客户端的主机名。
  • int getContentLength():获取请求正文的长度。
  • String getContentType():获取请求正文的 MIME 类型。
  • InputStream getInputStream():获取请求正文的输入流。

ServletResponse 接口代表 Servlet 对客户端的响应,并提供了设置响应内容和发送响应的方法。以下是一些常用的方法:

  • void setContentType(String type):设置响应的内容类型。
  • String getContentType():获取响应的内容类型。
  • void setContentLength(int len):设置响应正文的长度。
  • void setCharacterEncoding(String charset):设置响应字符编码。
  • PrintWriter getWriter():获取一个可以向客户端发送字符数据的 PrintWriter 对象。
  • ServletOutputStream getOutputStream():获取一个可以向客户端发送二进制数据的 ServletOutputStream 对象。
  • void sendRedirect(String location):重定向响应到指定的 URL。
  • void setStatus(int sc):设置响应的状态码。
  • void setHeader(String name, String value):设置响应头的值。
  • void addHeader(String name, String value):添加响应头的值。

本章用到的接口就是上面这些了,下面来看看本章代码的设计

Servlet容器代码设计

本章内容中加入servlet的设计,立个规定:servlet的请求均以“/servlet”打头,格式为“/servlet/servletName”,其他请求皆认为还是请求静态资源。

本章通过两个小应用程序说明如何开发自己的servlet容器,第一个应用程序的设计非常简单,仅仅用于说明servlet容器是如何运行的。它然后演变为第二个servlet容器,后者稍微复杂一点。

第一个应用程序

借用一下书中的UML图,来看下本次代码设计

区别于第一章的代码,本章代码做了如下改造

  • Request类实现了javax.servlet.ServletRequest接口
  • Response类实现了javax.servlet.ServletResponse接口
  • HttpServer1中能够同时接收动态与静态资源的请求,动态资源交给ServletProcessor1处理,静态资源交给StaticResourceProcessor处理。

接下来看看具体的代码

启动类HttpServer1

package ex02.hml.one;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpServer1 {

    // 声明一个结束标识,用来判断是否需要终止服务
    public static boolean shutDown = false;

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8080, 10, InetAddress.getByName("127.0.0.1"));
            Socket socket;
            while (!shutDown) {
                socket = serverSocket.accept();
                InputStream inputStream = socket.getInputStream();
                Request request = new Request();
                request.parse(inputStream);
                OutputStream outputStream = socket.getOutputStream();
                Response response = new Response(request, outputStream);

                if (request.getUri().startsWith("/servlet/")) {
                    ServletProcessor1 servletProcessor1 = new ServletProcessor1();
                    servletProcessor1.process(request, response);
                } else {
                    StaticResourceProcessor staticResourceProcessor = new StaticResourceProcessor();
                    staticResourceProcessor.process(request, response);
                }

                inputStream.close();
                socket.close();

                // 判断是否是结束服务的请求
                shutDown = request.getUri().equals("/shutdown");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


}

Request类

因为Servlet接收的request相关参数为ServletRequest,所以第一章的Request类也要实现ServletRequest接口,但是接口的方法暂时都不做实现,都返回空。原第一章的代码仍然保留。

package ex02.hml.one;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;

public class Request implements ServletRequest {

    private String uri;

    public void parse(InputStream inputStream) {

        try {
            byte[] bytes = new byte[1024];
            int readLenth = inputStream.read(bytes);
            String content = "";
            while (readLenth != -1) {
                content += new String(bytes);
                if (readLenth < 1024) {
                    break;
                }
                readLenth = inputStream.read(bytes);
            }
            System.out.println("request body -------------->");
            System.out.println(content);
            //获取请求内容中第一个空格和第二个空格之间的内容
            setUri(content);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void setUri(String content) {
        int index1 = content.indexOf(" ");
        if (index1 == -1) {
            return;
        }
        int index2 = content.indexOf(" ", index1 + 1);
        String substring = content.substring(index1 + 1, index2);
        this.uri = substring;
    }

    public String getUri() {
        return uri;
    }

    /*下面是实现 ServletRequest 接口的方法 */
    @Override
    public Object getAttribute(String s) {
        return null;
    }

    @Override
    public Enumeration getAttributeNames() {
        return null;
    }

    @Override
    public String getCharacterEncoding() {
        return null;
    }

    @Override
    public void setCharacterEncoding(String s) throws UnsupportedEncodingException {

    }

    @Override
    public int getContentLength() {
        return 0;
    }

    @Override
    public String getContentType() {
        return null;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return null;
    }

    @Override
    public String getParameter(String s) {
        return null;
    }

    @Override
    public Enumeration getParameterNames() {
        return null;
    }

    @Override
    public String[] getParameterValues(String s) {
        return new String[0];
    }

    @Override
    public Map getParameterMap() {
        return null;
    }

    @Override
    public String getProtocol() {
        return null;
    }

    @Override
    public String getScheme() {
        return null;
    }

    @Override
    public String getServerName() {
        return null;
    }

    @Override
    public int getServerPort() {
        return 0;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return null;
    }

    @Override
    public String getRemoteAddr() {
        return null;
    }

    @Override
    public String getRemoteHost() {
        return null;
    }

    @Override
    public void setAttribute(String s, Object o) {

    }

    @Override
    public void removeAttribute(String s) {

    }

    @Override
    public Locale getLocale() {
        return null;
    }

    @Override
    public Enumeration getLocales() {
        return null;
    }

    @Override
    public boolean isSecure() {
        return false;
    }

    @Override
    public RequestDispatcher getRequestDispatcher(String s) {
        return null;
    }

    @Override
    public String getRealPath(String s) {
        return null;
    }
}

Response类

因为Servlet接收的response相关参数为ServletResponse,所以第一章的Response类也要实现ServletResponse接口,接口的实现方法中仅实现一个getWriter方法,其他方法留空。原第一章的代码仍然保留。

package ex02.hml.one;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.*;
import java.util.Locale;

public class Response implements ServletResponse {

    private Request request;

    private OutputStream outputStream;

    private PrintWriter writer;

    public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";

    public Response(Request request, OutputStream outputStream) {
        this.request = request;
        this.outputStream = outputStream;
    }

    public void sendStaticResource() {
        try {
            if (request.getUri().equals("/shutdown")) {
                String msg = "HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html\r\n" +
                        "Content-Length: 32\r\n" +
                        "\r\n" +
                        "<h1>server already shutdown</h1>";
                outputStream.write(msg.getBytes());
                return;
            }


            File file = new File(rootDir + request.getUri());
            if (file.exists()) {
                FileInputStream fileInputStream = new FileInputStream(file);
                byte[] bytes = new byte[fileInputStream.available()];
                fileInputStream.read(bytes);
                String successMsg = "HTTP/1.1 200 OK\r\n" +
                        "Content-Type: text/html\r\n" +
                        "Content-Length: " + bytes.length + "\r\n" +
                        "\r\n";
                outputStream.write(successMsg.getBytes());
                outputStream.write(bytes);
                fileInputStream.close();
            } else {
                String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                        "Content-Type: text/html\r\n" +
                        "Content-Length: 23\r\n" +
                        "\r\n" +
                        "<h1>File Not Found</h1>";
                outputStream.write(errorMessage.getBytes());
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


    }


    @Override
    public String getCharacterEncoding() {
        return null;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return null;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        writer = new PrintWriter(outputStream, false);
        return writer;
    }

    @Override
    public void setContentLength(int i) {

    }

    @Override
    public void setContentType(String s) {

    }

    @Override
    public void setBufferSize(int i) {

    }

    @Override
    public int getBufferSize() {
        return 0;
    }

    @Override
    public void flushBuffer() throws IOException {

    }

    @Override
    public void resetBuffer() {

    }

    @Override
    public boolean isCommitted() {
        return false;
    }

    @Override
    public void reset() {

    }

    @Override
    public void setLocale(Locale locale) {

    }

    @Override
    public Locale getLocale() {
        return null;
    }
}

getWriter() 方法是提供给 servlet 获取 PrintWriter 对象的,PrintWriter对象可以将各种内容写入到Socket 的 OutputStream 中。

代码中使用了这个构造函数:public PrintWriter(OutputStream out, boolean autoFlush) ,第二个参数传true时,调用 println, printf, format 三个方法会自动刷新(flush),我这里的代码不同与书中的,我将它设为了false, 需要刷新输出流时,手动刷新,防止遗漏刷新过程。

StaticResourceProcessor类

这是处理静态资源的类,此类很简单,静态资源的处理逻辑不变,仍然放在Response类中

public class StaticResourceProcessor {
    public void process(Request request,Response response) {
        response.sendStaticResource();
    }
}

ServletProcessor1类

Servlet处理器,或者也可以叫它Servlet分发类,它负责将HTTP请求分发到指定的Servlet中。

process方法中需要创建类加载器并加载需要用到的 servlet ,然后才能使用它 。

package ex02.hml.one;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 处理servlet请求   /servlet/servletName
 *
 */
public class ServletProcessor1 {

    public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";

    public void process(Request request, Response response) {
        try {
            String uri = request.getUri();
            String servletName = uri.substring(uri.lastIndexOf("/") + 1);

            //首先获取类加载器,这里采用URLClassLoader
            File file = new File(rootDir);
            String repository = (new URL("file", null, file.getCanonicalPath() + File.separator)).toString();
            URL[] urls = new URL[1];
            urls[0] = new URL(null, repository);
            URLClassLoader urlClassLoader = new URLClassLoader(urls);

            //加载servlet对应的类
            Class<?> aClass = urlClassLoader.loadClass(servletName);
            Servlet servlet = (Servlet) aClass.newInstance();

            // 调用servlet的service方法,处理请求并返回结果
            servlet.service(request, response);

        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException |
                 ServletException e) {
            e.printStackTrace();
        }

    }
}

PrimitiveServlet类

这就是一个具体的 servlet 了,实现了 Servlet 接口,这里仅仅实现了 service 一个方法,其他方法留空。

这个servlet类没有放在项目的src目录下,而是放在了webroot目录下,也就是说它不属于Tomcat的源码范围。使用过Tomcat部署项目的同学应该了解,servlet应该是我们自己根据各个项目的业务写出来的,项目打包后放在Tomcat的webapps目录下,Tomcat启动后自动扫描webapps目录下的项目,所以PrimitiveServlet也暂时放到一个非源码目录下。

PrimitiveServlet_origin.java是原书中的PrimitiveServlet类的源码,我给原书中的类都加了一个“_origin”后缀,但是里面的类名没改,所以不能编译,仅做对比参考。

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;


public class PrimitiveServlet implements Servlet {

    public void init(ServletConfig config) throws ServletException {
        System.out.println("init");
    }

    public void service(ServletRequest request, ServletResponse response)
            throws ServletException, IOException {
        System.out.println("from service");
        PrintWriter out = response.getWriter();

        String body = "This is the response data。";
        String header = "HTTP/1.1 200 OK\r\n" +
                "Content-Type: text/html\r\n" +
                "Content-Length: " + body.length() + "\r\n" +
                "\r\n";

        out.print(header);
        out.print(body);

        out.flush();
        out.close();
    }

    public void service_chunked(ServletRequest request, ServletResponse response)
            throws ServletException, IOException {
        System.out.println("from service");
        PrintWriter out = response.getWriter();

        String msg = "HTTP/1.1 200 OK\r\n" +
                "Content-Type: text/plain\r\n" +
                "Transfer-Encoding: chunked\r\n" +
                "\r\n";
        out.print(msg);

        String msg1 = "This is the data in the first chunk。\r\n";
        out.print(Integer.toHexString(msg1.length()) + "\r\n");
        out.print(msg1 + "\r\n");

        String msg2 = "This is the data in the second chunk。\r\n";
        out.print(Integer.toHexString(msg2.length()) + "\r\n");
        out.print(msg2 + "\r\n");

        out.print("0\r\n\r\n");
        out.flush();
        out.close();
    }

    public void destroy() {
        System.out.println("destroy");
    }

    public String getServletInfo() {
        return null;
    }

    public ServletConfig getServletConfig() {
        return null;
    }

}

service方法我提供了两种实现,区别是在HTTP返回头中指定数据长度的方式不同,一种是使用 Content-Length 声明返回body体的总长度;另一种是使用 Transfer-Encoding: chunked 的方式分块声明每块数据的长度。两种方式皆可成功返回数据。

使用Content-Length方式的话,需要提前知道完整body体后才可拼写HTTP Header。

使用Transfer-Encoding: chunked 的话,不需要在Header中指定长度,只要在body体中声明每块数据的长度即可。

在HTTP返回格式的定义上也有些许不同

使用Content-Length时

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
\r\n
Hello World

使用 Transfer-Encoding: chunked 时

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
\r\n
内容1长度(16进制数)\r\n
内容1\r\n
内容2长度(16进制数)\r\n
内容2\r\n
0\r\n\r\n (结尾必须是0和空内容,组合起来就是 0\r\n\r\n)

编译PrimitiveServlet类

在项目的webroot目录下有个complie.sh文件,里面写有可以编译PrimitiveServlet类的脚本,脚本如下

javac -cp "../lib/*" PrimitiveServlet.java

最终效果展示

启动HttpServer1,浏览器访问效果展示

至此,一个非常简易的servlet容器就完成了。但是这个程序也有一点问题,那就是ServletProcessor1在调用具体servlet的service方法时,将ex02.hml.one.Request与ex02.hml.one.Response对象传了过去,虽然PrimitiveServlet的service方法的入参类型是ServletRequest与ServletResponse,但是了解程序设计的人仍然能将它们向下转型为Request与Response对象,并调用其public方法,如Requst的 parse 方法与Response的sendStaticResource方法,这是违反我们的设计初衷的。怎么解决呢?使用外观类。借用一下原书中的UML图

从这两个外观类开始,下面进行本章第二个应用程序的设计

第二个应用程序

RequestFacade与ResponseFacade

RequestFacade类实现了ServletRequest接口并持有一个ServletRequest对象的引用->request,这个request属性用来存放我们的ex02.hml.two.Request对象。RequestFacade类中实现的ServletRequest接口方法的具体实现都调用request属性的对应实现方法。这样下来,RequestFacade类中就没有暴漏多余的方法了。即使servlet程序员将ServletRequest对象向下转型为RequestFacade对象,也访问不了Request类中的parse、getUri方法了。

ResponseFacade同理,实现了ServletResponse接口并持有一个ServletResponse对象的引用->response,这个response属性用来存放我们的ex02.hml.two.Response对象。

package ex02.hml.two;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;

public class RequestFacade implements ServletRequest {

    private ServletRequest request;

    public RequestFacade(Request request) {
        this.request = request;
    }
    
    @Override
    public Object getAttribute(String s) {
        return null;
    }

    @Override
    public Enumeration getAttributeNames() {
        return null;
    }

    @Override
    public String getCharacterEncoding() {
        return null;
    }

    @Override
    public void setCharacterEncoding(String s) throws UnsupportedEncodingException {

    }

    @Override
    public int getContentLength() {
        return 0;
    }

    @Override
    public String getContentType() {
        return null;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return null;
    }

    @Override
    public String getParameter(String s) {
        return null;
    }

    @Override
    public Enumeration getParameterNames() {
        return null;
    }

    @Override
    public String[] getParameterValues(String s) {
        return new String[0];
    }

    @Override
    public Map getParameterMap() {
        return null;
    }

    @Override
    public String getProtocol() {
        return null;
    }

    @Override
    public String getScheme() {
        return null;
    }

    @Override
    public String getServerName() {
        return null;
    }

    @Override
    public int getServerPort() {
        return 0;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return null;
    }

    @Override
    public String getRemoteAddr() {
        return null;
    }

    @Override
    public String getRemoteHost() {
        return null;
    }

    @Override
    public void setAttribute(String s, Object o) {

    }

    @Override
    public void removeAttribute(String s) {

    }

    @Override
    public Locale getLocale() {
        return null;
    }

    @Override
    public Enumeration getLocales() {
        return null;
    }

    @Override
    public boolean isSecure() {
        return false;
    }

    @Override
    public RequestDispatcher getRequestDispatcher(String s) {
        return null;
    }

    @Override
    public String getRealPath(String s) {
        return null;
    }
}
package ex02.hml.two;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;

public class ResponseFacade implements ServletResponse {

    private ServletResponse response;

    public ResponseFacade(Response response) {
        this.response = response;
    }

    @Override
    public String getCharacterEncoding() {
        return null;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return null;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return response.getWriter();
    }

    @Override
    public void setContentLength(int i) {

    }

    @Override
    public void setContentType(String s) {

    }

    @Override
    public void setBufferSize(int i) {

    }

    @Override
    public int getBufferSize() {
        return 0;
    }

    @Override
    public void flushBuffer() throws IOException {

    }

    @Override
    public void resetBuffer() {

    }

    @Override
    public boolean isCommitted() {
        return false;
    }

    @Override
    public void reset() {

    }

    @Override
    public void setLocale(Locale locale) {

    }

    @Override
    public Locale getLocale() {
        return null;
    }
}

HttpServer2类

HttpServer2类与HttpServer1类相似,只是在main方法中会使用ServletProcessor2类,而不是ServletProcessor1类

package ex02.hml.two;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpServer2 {

    public static boolean shutDown = false;

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8080, 10, InetAddress.getByName("127.0.0.1"));
            Socket socket;
            while (!shutDown) {
                socket = serverSocket.accept();
                InputStream inputStream = socket.getInputStream();
                Request request = new Request();
                request.parse(inputStream);
                OutputStream outputStream = socket.getOutputStream();
                Response response = new Response(request, outputStream);

                if (request.getUri().startsWith("/servlet/")) {
                    ServletProcessor2 servletProcessor2 = new ServletProcessor2();
                    servletProcessor2.processor(request, response);
                } else {
                    StaticResourceProcessor staticResourceProcessor = new StaticResourceProcessor();
                    staticResourceProcessor.process(request, response);
                }

                inputStream.close();
                socket.close();
                shutDown = request.getUri().equals("/shutdown");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


}

ServletProcessor2类

ServletProcessor2类与ServletProcessor1类相似,只是在process方法中有一点不同

package ex02.hml.two;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * 处理servlet请求   /servlet/servletName
 */
public class ServletProcessor2 {

    public static final String rootDir = System.getProperty("user.dir") + File.separatorChar + "webroot";

    public void processor(Request request, Response response) {
        try {
            String uri = request.getUri();
            String servletName = uri.substring(uri.lastIndexOf("/") + 1);

            //首先获取类加载器
            File file = new File(rootDir);
            String repository = (new URL("file", null, file.getCanonicalPath() + File.separator)).toString();
            URL[] urls = new URL[1];
            urls[0] = new URL(null, repository);
            URLClassLoader urlClassLoader = new URLClassLoader(urls);

            //加载servlet对应的类
            Class<?> aClass = urlClassLoader.loadClass(servletName);
            Servlet servlet = (Servlet) aClass.newInstance();

            RequestFacade requestFacade = new RequestFacade(request);
            ResponseFacade responseFacade = new ResponseFacade(response);
            
            servlet.service(requestFacade, responseFacade);

        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException |
                 ServletException e) {
            e.printStackTrace();
        }
        
    }
}

使用HttpServer2启动服务后的浏览器访问结果与HttpServer1相同,就不再贴图了。

好了一个稍加改造后的servlet容器就完成了,本章内容就到这里,下一章我们来实现一个精简版的连接器,敬请期待!

源码分享

https://gitee.com/huo-ming-lu/HowTomcatWorks

本章代码我按照两个应用程序分别放在了 one、two 两个包下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/546479.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux基础|线程池Part.1|线程池的定义和运行逻辑

线程池的定义和运行逻辑 多线程的问题&#xff1a; 如果并发的线程数量很多&#xff0c;并且每个线程都是执行一个时间很短的任务就结束了&#xff0c;这样频繁创建线程就会大大降低系统的效率&#xff0c;因为频繁创建线程和销毁线程需要时间。 那么一个很自然的想法就出现了…

杂货铺 | Linux虚拟机Ubuntu操作系统下设置共享文件夹(以及找不到hgfs文件夹怎么办)

文章目录 &#x1f4da;步骤一&#xff1a;配置共享文件夹&#x1f4da;步骤二&#xff1a;配置挂载环境&#x1f4da;步骤三&#xff1a;解决权限问题&#x1f4da;步骤四&#xff1a;解决重启失效问题 &#x1f4da;步骤一&#xff1a;配置共享文件夹 建立本地共享文件夹&…

Mysql的事务隔离级别以及事务的四大特性。

MySQL 的事务隔离级别是数据库管理系统中的一个重要概念&#xff0c;它决定了事务如何隔离和影响其他并发事务。MySQL 支持四种事务隔离级别&#xff0c;分别是&#xff1a;读未提交&#xff08;READ UNCOMMITTED&#xff09;、读已提交&#xff08;READ COMMITTED&#xff09;…

Qt QStyle详解

1.简介 QStyle类是 Qt 框架中用于控制应用程序界面元素外观的一个抽象基类。这个类提供了一种方式来定制窗口部件&#xff08;widgets&#xff09;的绘制和行为&#xff0c;可以通过改变主题或风格来更改应用程序的外观&#xff0c;而无需修改窗口部件本身的代码。 Qt包含一组…

一种基于OpenCV的图片倾斜矫正方法

需求描述&#xff1a; 对倾斜的图片进行矫正&#xff0c;返回倾斜角度和矫正后的图片。 解决方法&#xff1a; 1、各种角度点被投影到一个累加器阵列中&#xff0c;其中倾斜角度可以定义为在最大化对齐的搜索间隔内的投影角度。 2、以不同的角度旋转图像&#xff0c;并为每…

Excel文件解析

在此模块的学习中&#xff0c;我们需要一个新的开源类库---Apahche POI开源类库。这个类库的用途是&#xff1a;解析并生成Excel文件(Word、ppt)。Apahche POI基于DOM方式进行解析&#xff0c;将文件直接加载到内存&#xff0c;所以速度比较快&#xff0c;适合Excel文件数据量不…

学习经验分享【32】本科/硕士开题报告、中期报告等写作经验分享

本科/硕士阶段首先就是要写开题报告&#xff0c;然后中期报告&#xff0c;这篇博文就是分享一下写报告的经验&#xff0c;避免被老师打回来。本人有丰富的写报告经验&#xff0c;有需要的朋友可添加文末联系方式与我联系。 一、本科开题报告的提纲 课题来源及研究的目的和意义…

js性能优化(五)

第五章开始啦~~~~~~~~~~~~~ 防抖和节流之前自己有学过一次&#xff0c;包括几种方式怎么实现&#xff0c;代码如何写花了两天有写过&#xff0c;这次算是更系统的一个复习加填补 十七、防抖与节流 为什么需要防抖和节流&#xff1a; 在一些高频率事件触发的场景下我们不希望…

51单片机

STC89C52 一.定时器 1.介绍 2.计时 2.定时器寄存器  2.1 定时器控制寄存器TCON  2.2 定时器模式寄存器TMOD  2.3 定时器如何定时10毫秒  2.4 定时器寄存器配置    2.4.1 TCON    2.4.2 TMOD    2.4.3 实现    2.4.5 按位操作 3.定时器中断  3.1 定…

d盘无法格式化说另一个正在使用怎么办

在日常生活和工作中&#xff0c;我们经常会遇到需要对电脑硬盘进行格式化的情况。然而&#xff0c;有时在尝试格式化D盘时&#xff0c;会遇到一个常见的错误提示&#xff1a;“另一个程序正在使用此文件&#xff0c;因此无法进行操作”。这个提示可能会让许多人感到困惑&#x…

《自动机理论、语言和计算导论》阅读笔记:p172-p224

《自动机理论、语言和计算导论》学习第 8 天&#xff0c;p172-p224总结&#xff0c;总计 53 页。 一、技术总结 1.Context-Free Grammar(CFG) 2.parse tree (1)定义 p183&#xff0c;But perhaps more importantly, the tree, known as a “parse tree”, when used in a …

C语言基础入门案例(1)

目录 第一题&#xff1a;实现大衍数列的打印 第二题&#xff1a;生成所有由1、2、3、4组成的互不相同且无重复数字的三位数&#xff0c;并计算总数 第三题&#xff1a;整数加法计算器 第四题&#xff1a;实现一个范围累加和函数 第五题&#xff1a;编写一个函数计算整数的阶…

Vue3基础笔记(3)高级绑定

一.Class绑定 数据绑定的一个常见需求场景师操纵元素的CSS class列表&#xff0c;因为class是attribute&#xff0c;我们可以和其他attribute一样使用v-bind将他们和动态的字符串绑定&#xff0c;但是在处理较为复杂的绑定时&#xff0c;拼接字符串容易出现错误。因此Vue专门为…

ZJJ-2A直流绝缘监视继电器额定电流3.1mA额定电压110VDCJOSEF约瑟

系列型号 JJJ-1绝缘监视继电器&#xff1b; ZJJ-1/A绝缘监视继电器&#xff1b; ZJJ-1A绝缘监视继电器&#xff1b; ZJJ-2型直流绝缘监视继电器 ZJJ-2直流绝缘监视继电器&#xff1b; ZJJ-2B直流绝缘监视继电器&#xff1b; ZJJ-2AC直流绝缘监视继电器&#xff1b; 用途…

【基础物理实验】【AFM虚拟实验】基于AFM的物质表面微观结构及力学性质表征仿真实验(上)【北京航空航天大学】

基于AFM的物质表面微观结构及力学性质表征仿真实验 说明&#xff1a; 本次实验为本科生《基础物理实验》课程中的虚拟实验部分&#xff0c;在虚拟实验平台中进行。 一、实验目的&#xff1a; 1. 掌握AFM的基本成像原理及系统结构&#xff1b; 2. 掌握AFM的基本操作技巧及操…

什么是云安全

云安全和网络安全有所不同&#xff0c;因为云安全一词 比网络安全更涵盖整个企业基础设施。一般来说&#xff0c;当人们提到云安全时&#xff0c;指的是第三方服务提供商提供的 IaaS 云环境。在这种情况下&#xff0c;云安全不仅包括网络安全工具&#xff0c;还包括服务器、容器…

数据结构7:栈

文章目录 头文件Stack.h 实现文件 测试文件test.c 经典题目 头文件 Stack.h #pragma once #include<stdio.h> #include<stdlib.h> #include<string.h> #include<assert.h>#define CAPACITY_INIT 4typedef int STDataType;typedef struct Stack {STDa…

云端运维:我的成长轨迹与荣耀印记

云端运维&#xff1a;我的成长轨迹与荣耀印记 关于博主 ​ 我是一名运维领域的博客平台博主&#xff0c;也是一名对技术充满热情的探索者。在运维这条道路上&#xff0c;我不断积累知识、提升技能&#xff0c;力求将最新的技术理念和实践经验分享给更多志同道合的朋友。我热衷…

Python代码打包成exe程序

国内镜像源 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple阿里云 https://mirrors.aliyun.com/pypi/simple/豆瓣 https://pypi.douban.com/simple/ 百度云 https://mirror.baidu.com/pypi/simple/中科大 https://pypi.mirrors.ustc.edu.cn/simple/华为云 https://mirror…

Jmeter参数化的 4 种方式用法总结

参数化就是用变量代替数据的过程&#xff0c;总结参数化的4种方式&#xff1a; 1、用户自定义变量 用户自定义变更有两种方法&#xff1a; &#xff08;1&#xff09;在测试计划面板中的用户定义的变量设置 说明&#xff1a;在此用户定义的变量对所有测试计划都会生效 &…