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

news2024/12/26 21:58:02

现在开始《深入剖析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 两个包下

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FFmpeg: 自实现ijkplayer播放器--04消息队列设计

文章目录 播放器状态转换图播放器状态对应的消息&#xff1a; 消息对象消息队列消息队列api插入消息获取消息初始化消息插入消息加锁初始化消息设置消息参数消息队列初始化清空消息销毁消息启动消息队列终止消息队列删除消息 消息队列&#xff0c;用于发送&#xff0c;设置播放…

docker-compose yaml指定具体容器网桥ip网段subnet;docker创建即指定subnet;docker取消自启动

1、docker-compose yaml指定具体容器网桥ip网段subnet docker-compose 启动yaml有时可能的容器网段与宿主机的ip冲突导致宿主机上不了网&#xff0c;这时候可以更改yaml指定subnet 宿主机内网一般是192**&#xff0c;这时候容器可以指定172* version: 3.9 services:coredns:…

内网渗透-域环境的搭建

域环境的搭建 文章目录 域环境的搭建前言一、什么是域环境 什么是域内网基础知识点 二、域环境的搭建 1. 部署域结构2.如何加入域3.SRV出错及解决办法4.SRV记录注册不成功的可能原因 禁用域中的账户将计算机退出域添加域用户总结 前言 一、什么是域环境 什么是域 域是一种管…

盘点2024年最新可用免费云服务器

随着云计算技术的快速发展&#xff0c;越来越多的企业和个人开始使用云服务器来满足各种业务需求。云服务器作为云计算的核心服务之一&#xff0c;以其弹性扩展、按需付费等特点受到广泛关注。本文将为大家盘点2024年最新可用免费云服务器&#xff0c;助力大家轻松上云&#xf…

浏览器工作原理与实践--CSRF攻击:陌生链接不要随便点

在上一篇文章中我们讲到了XSS攻击&#xff0c;XSS 的攻击方式是黑客往用户的页面中注入恶意脚本&#xff0c;然后再通过恶意脚本将用户页面的数据上传到黑客的服务器上&#xff0c;最后黑客再利用这些数据进行一些恶意操作。XSS攻击能够带来很大的破坏性&#xff0c;不过另外一…

量子飞跃:从根本上改变复杂问题的解决方式

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 编辑丨王珩 编译/排版丨沛贤 深度好文&#xff1a;1000字丨5分钟阅读 利用多功能量子比特的量子计算机已处于解决复杂优化问题的最前沿&#xff0c;例如旅行商问题&#xff0c;这是一个典型的…

Python实现时间序列ARIMA模型(附带超详细理论知识和完整代码实现)

文章目录 0 结果1 介绍2 建模2.1 预备知识2.1.1 ADF检验结果&#xff08;单位根检验统计量&#xff09;2.1.2 差分序列的白噪声检验&#xff08;这里使用Ljung-Box检验&#xff09;2.1.3 ARIMA模型&#xff08;差分整合移动平均自回归模型&#xff09;的三个参数:p&#xff0c;…

探索分布式技术--------------注册中心zookeeper

目录 一、ZooKeeper是什么 二、ZooKeeper的工作机制 三、ZooKeeper特点 四、ZooKeeper数据结构 五、ZooKeeper应用场景 5.1统一命名服务 5.2统一配置管理 5.3统一集群管理 5.4服务器动态上下线 5.5软负载均衡 六、ZooKeeper的选举机制 6.1第一次启动选举机制 6.2非…

家居网购项目(Ajax验证用户名+上传图片)

文章目录 1.Ajax验证用户名1.程序框架图2.修改MemberServlet3.修改login.jsp4.结果展示 2.Ajax判断验证码是否输入正确1.修改MemberServlet2.修改login.jsp3.结果展示 3.Ajax添加购物车1.程序框架图2.修改CartServlet2.修改index.jsp3.解决问题—未登录直接添加购物车&#xff…

Excel文件解析

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

没有算法大佬,都是草台班子

没有算法大佬&#xff0c;都是草台班子。 最近除了工作之外&#xff0c;还有一些时间在和加我微信的小伙伴沟通&#xff0c;聊的内容大部分集中在如何快速有效的学习人工智能、入门人工智能的技巧。 其中&#xff0c;一个知乎过来加我微信的小伙伴的经历更是让我感触很深。 …

eclipse导入maven项目与配置使用本地仓库

前言 本人润国外了&#xff0c;发现不能用收费软件IDEA了&#xff0c;需要使用eclipse&#xff0c;这个免费。 但是早忘了怎么用了&#xff0c;在此总结下。 一、eclipse导入本地项目 1.选这个&#xff1a;open projects from file system… 2.找到项目文件夹&#xff0c;…

树莓派点亮双色LED

双色LED灯准确来说叫双基色LED灯,是指模块只能显示2种颜色,一般是红色和绿色,可以有三种状态 :灭,颜色1亮,颜色2亮,根据颜色组合的不同,分为红蓝双色,黄蓝双色,红绿双色等等。 接线:将引脚S(绿色)和中间引脚(红色)连接到Raspberry Pi的GPIO接口上,对Raspberry…

《QT实用小工具·二十六》运行时间记录

1、概述 源码放在文章末尾 运行时间记录&#xff0c;包含如下功能&#xff1a; 可以启动和停止服务&#xff0c;在需要的时候启动。 可以指定日志文件存放目录。 可以指定时间日志输出间隔。 可以单独追加一条记录到日志文件。 日志为文本格式&#xff0c;清晰明了。 软…

JavaScript排序大揭秘:手绘图解6大常见排序算法,一网打尽

前言 &#x1f4eb; 大家好&#xff0c;我是南木元元&#xff0c;热爱技术和分享&#xff0c;欢迎大家交流&#xff0c;一起学习进步&#xff01; &#x1f345; 个人主页&#xff1a;南木元元 本文用图解总结梳理了6种常见的排序算法 &#xff0c;如下&#x1f447;&#xff1…

【热门话题】PyTorch:深度学习领域的强大工具

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 PyTorch&#xff1a;深度学习领域的强大工具一、PyTorch概述二、PyTorch核心特性…

WGCNA分析

目录 基本概念 基本原理 下游分析 基本概念 WGCNA其译为加权基因共表达网络分析。该分析方法旨在寻找协同表达的基因模块(module)&#xff0c;并探索基因网络与关注的表型之间的关联关系&#xff0c;以及网络中的核心基因。 适用于复杂的数据模式&#xff08;推荐5组(或者…

JavaEE企业开发新技术5

目录 2.18 综合应用-1 2.19 综合应用-2 2.20 综合应用-3 2.21 综合应用-4 2.22 综合应用-5 Synchronized &#xff1a; 2.18 综合应用-1 反射的高级应用 DAO开发中&#xff0c;实体类对应DAO的实现类中有很多方法的代码具有高度相似性&#xff0c;为了提供代码的复用性,降低…

Zookeeper与Kafka消息队列

目录 一、Zookeeper 1、zookeeper简介 2、zookeeper的特点 3、zookeeper的工作模式跟工作机制 3.1 工作模式&#xff1a; 3.2工作机制&#xff1a;​编辑 4、zookeeper应用场景及选举机制 4.1 应用场景&#xff1a; 4.2 选举机制&#xff1a; 4.2.1第一次启动选举机制…

<计算机网络自顶向下> CDN

视频服务挑战 规模性异构性&#xff1a;不同用户有不同的能力&#xff08;比如有线接入和移动用户&#xff1b;贷款丰富和受限用户&#xff09;解决方法是&#xff1a;分布式的应用层面的基础设施CDN 多媒体&#xff1a;视频 视频是固定速度显示的一系列图像的序列&#xff…