第3章 WebServer重构

3.1 重构原生Web服务框架

3.1.1 分析原生Web服务框架

在服务端代码的 ClientHandler 中,请求解析、处理请求、返回响应的代码混杂在一起,这样的设计会导致代码难以维护和理解。为了提高代码的可读性、可维护性和可扩展性,我们需要对这些代码进行重构,并按照功能抽取对应的类,从而使后续的开发和维护更加方便。

分析目前的代码:

重构后软件的整体结构如下图:

重构后的结构主要包含以下几个主要组件:

  1. ClientHandler(Web处理线程):该组件是处理客户端请求的主要线程。它接收客户端发送的HTTP请求,并将请求交给HttpServletRequest进行解析,然后将解析得到的请求信息传递给DispatcherServlet进行核心请求处理。最后,将处理得到的响应信息传递给HttpServletResponse进行缓存和发送给客户端。

  2. HttpServletRequest(请求解析和封装):负责解析和封装客户端发送的HTTP请求的信息,包括请求方法、URL、请求头和请求体等。它将原始的HTTP请求转换为一个请求对象,以方便后续处理逻辑使用。

  3. HttpServletResponse(响应缓存和处理):负责缓存和处理向客户端发送的响应信息。它将处理得到的响应内容暂存起来,并在合适的时机发送给客户端。

  4. DispatcherServlet(请求分发器):封装了核心请求处理逻辑。当ClientHandler接收到客户端请求后,将请求信息传递给DispatcherServlet。它根据请求的URL和方法,决定调用哪个业务处理模块来处理请求,最终得到处理结果。

重构后的结构将不同功能的代码分离到独立的组件中,增强了代码的可读性和可维护性。HttpServletRequest负责解析和封装请求信息,HttpServletResponse负责缓存和处理响应信息,而DispatcherServlet作为请求分发器,负责将请求分发给相应的业务处理模块进行处理。这样的设计使得代码逻辑更加清晰,方便后续的开发和维护。

3.1.2 重构请求

为了提高代码的模块化和清晰性,我们将请求部分的代码抽取到一个新的类HttpServletRequest中,该类封装了HTTP请求的解析逻辑,并提供了访问解析结果的方法。

  1. 定义类HttpServletRequest,封装HTTP请求的逻辑。该类包括了以下成员变量:
  • Socket socket: 保存客户端和服务器之间的网络连接
  • String method: 保存HTTP请求的方法,如GET、POST等
  • String uri: 保存HTTP请求的URI,即请求的资源路径
  • String protocol: 保存HTTP请求使用的协议,如HTTP/1.1、HTTP/2.0等
  • HashMap<String, String> headers: 保存HTTP请求头的所有内容,以键值对的形式存储
  1. 定义属性访问方法
  • public String getMethod():返回HTTP请求的方法
  • public String getUri():返回HTTP请求的URI
  • public String getProtocol():返回HTTP请求使用的协议
  1. 定义方法private void parseRequestLine(),用于解析HTTP请求的请求行,包括请求方法、URI和协议版本号,将解析结果输出到控制台,便于后续调试。

  2. 定义方法解析请求头以及获取请求头:

  • private void parseHeaders():解析HTTP请求头部的所有内容,以键值对的形式存储。
  • public String getHeader(String name):根据请求头的名称返回请求头的值
  1. 定义构造函数public HttpServletRequest(Socket socket):接受一个Socket对象作为参数,通过解析Socket中的输入流,初始化该类的成员变量。并定义方法public String readLine(),用于从Socket的输入流中读取一行数据并返回。

HttpServletRequest类的完整代码示意如下:

java 复制代码
/*封装HTTP请求逻辑 */
public class HttpServletRequest {
    private Socket socket;

    private String method;
    private String uri;
    private String protocol;

    private HashMap<String, String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException {
        this.socket = socket;
        //解析请求行
        parseRequestLine();
        //解析请求头
        parseHeaders();
    }

    /**
     * 解析请求行方法
     * @throws IOException 网络出现异常
     */
    private void parseRequestLine() throws IOException{
        String requestLine = readLine();
        String[] parts = requestLine.split("\\s");
        method = parts[0];
        uri = parts[1];
        protocol = parts[2];
        System.out.println("解析请求行:"+requestLine);
        System.out.println("method:"+method);
        System.out.println("uri:"+uri);
        System.out.println("protocol:"+protocol);
    }

    /**
     * 解析请求头方法,将解析结构缓存到一个HashMap中
     * @throws IOException 网络出现错误
     */
    private void parseHeaders() throws IOException {
        while (true) {
            String line = readLine();
            //解析到空行结束
            if (line.isEmpty()) {
                break;
            }
            System.out.println("解析请求头:" + line);
            String[] parts = line.split(":\\s");
            headers.put(parts[0], parts[1]);
        }
        System.out.println("所有请求头:" + headers);
    }
    /**
     * 这段代码的作用是从Socket的输入流中读取一行数据并返回。它通过InputStream获取Socket的输入流,
     * 然后使用一个StringBuilder对象来存储读取的数据,最终返回读取的数据。
     * 具体实现逻辑如下:
     * 1. 创建一个InputStream对象in,并将其设置为socket的输入流。
     * 2. 创建一个StringBuilder对象builder,用于存储读取的数据。
     * 3. 定义两个字符变量previous和current,用于记录前一个字符和当前字符。
     * 4. 定义一个int类型变量b,用于记录从输入流中读取的字节。
     * 5. 使用while循环从输入流中读取字节,直到读取完一行数据。
     * 6. 将读取到的字节转换成字符类型,并赋值给变量current。
     * 7. 判断当前字符是否为行结束符("\r\n"),如果是则退出循环,否则将当前字符添加到builder中。
     * 8. 将当前字符赋值给previous,以备下次循环使用。
     * 9. 循环结束后,将builder转换成字符串并返回。
     * @return 从Socket的输入流中读取一行数据并返回
     * @throws IOException 出现网络IO错误
     */
    public String readLine() throws IOException{
        InputStream in = socket.getInputStream();
        StringBuilder builder= new StringBuilder();
        //   前一个字符  当前字符
        char previous = 0, current = 0;
        int b;
        //解析请求行
        while ((b=in.read())!=-1){
            current = (char) b;
            if (previous == '\r' && current == '\n'){
                //遇到行结束就结束读取
                break;
            }else if (current != '\r' && current != '\n'){
                builder.append(current);
            }
            previous = current;
        }
        return builder.toString();
    }
    /**
     * 获取当前的请求方式
     * @return 请求方式
     */
    public String getMethod() {
        return method;
    }

    /**
     * 获取当前请求的 uri
     * @return 请求资源路径
     */
    public String getUri() {
        return uri;
    }

    /**
     * 返回当前请求的 协议
     * @return 返回请求协议
     */
    public String getProtocol() {
        return protocol;
    }
    /**
     * 查询一个请求头
     * @param name 请求头名字
     * @return 请求头的值
     */
    public String getHeader(String name) {
        return headers.get(name);
    }
}
  1. 重构ClientHandler类,将解析请求部分替换为HttpServletRequest:
java 复制代码
//1. 解析请求
HttpServletRequest request = new HttpServletRequest(socket);
String uri = request.getUri();

通过重构后,现在ClientHandler类中的请求部分代码得到了简化,提高了代码的可读性和可维护性。同时,HttpServletRequest类封装了HTTP请求解析的逻辑,使得ClientHandler更专注于业务处理部分,使整体结构更清晰。这样的重构有助于提高代码的模块化和可维护性,方便后续的开发和维护。

3.1.3 重构响应

在进行请求部分的重构后,现在继续对响应逻辑进行重构,将响应代码抽取到HttpServletResponse类中,以优化ClientHandler。

  1. 定义了一个名为HttpServletResponse的类,封装HTTP响应的逻辑。包含:
  • socket:一个socket实例变量,用于表示客户端连接的套接字
  • statusCode:表示HTTP状态码,默认值为200
  • statusReason:表示HTTP状态描述,默认值为"OK"
  • contentFile:表示响应正文对应的实体文件

在构造函数中,将客户端的套接字作为参数,将其赋给socket实例变量。

  1. 添加方法setContentFile、setStatusCode和setStatusReason用于设置响应正文文件、状态码和状态描述,分别将它们赋给成员变量。

  2. 抽取println方法用于将一行数据发送到网络流中,首先通过socket的getOutputStream方法获取输出流,然后将数据转换为ISO_8859_1编码的字节数组,并发送回车符和换行符。

  3. 抽取send方法用于将HTTP响应发送给客户端:

  • 它首先根据状态码和状态描述拼接一个状态行,并发送给客户端
  • 然后发送响应头,包括Content-Type和Content-Length
  • 最后发送一个空行表示响应头已经发送完成
  • 通过FileInputStream读取contentFile中的数据,并通过OutputStream发送给客户端

HttpServletResponse类的完整代码示意如下:

java 复制代码
/*封装HTTP响应逻辑 */
public class HttpServletResponse {
    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;                   //状态代码
    private String statusReason = "OK";             //状态描述
    //响应头相关信息
    //响应正文相关信息
private File contentFile;                       //响应正文对应的实体文件

    public HttpServletResponse(Socket socket){
        this.socket = socket;
    }

    public void send() throws IOException {
        String statusLine = "HTTP/1.1 " + statusCode + " " + statusReason;
        //发送状态行
        println(statusLine);
        System.out.println("发送状态行: "+statusLine);
        //发送响应头
        println("Content-Type: text/html; charset=utf-8");
        println("Content-Length: " + contentFile.length());
        System.out.println("发送响应头: " + "Content-Length: " + contentFile.length());
        //发送空行
        println("");
        //将文件内容发送到浏览器
        FileInputStream in = new FileInputStream(contentFile);
        OutputStream out = socket.getOutputStream();
        byte[] buf = new byte[8*1024];
        int n;
        while ((n=in.read(buf))!=-1){
            out.write(buf, 0, n);
        }
    }

    public void setContentFile(File contentFile){
        this.contentFile = contentFile;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public void setStatusReason(String statusReason){
        this.statusReason = statusReason;
    }

    /**
     * 发送一行到网络流
     * @param line 一行
     * @throws IOException 网络故障
     */
    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        out.write('\r');//发送回车符
        out.write('\n');//发送换行符
    }
}

5.重构ClientHandler,使用HttpServletResponse类替换响应过程:

java 复制代码
//1. 解析请求
HttpServletRequest request = new HttpServletRequest(socket);
HttpServletResponse response = new HttpServletResponse(socket);
String uri = request.getUri();

//2. 发送响应
//根据找到静态资源
//类加载路径:target/classes
File root = new File(
        ClientHandler.class.getClassLoader().getResource(".").toURI()
);
//定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)
File staticDir = new File(root,"static");
//定位target/classes/static目录中的文件
File file = new File(staticDir,uri);

response.setContentFile(file);
response.send();
  1. 重构后ClientHandler的代码变得非常简洁,但是测试时候控制台出现了异常信息:

这个显然是浏览器在请求favicon.ico文件,然而我们的服务器端没有对应的资源造成的问题。解决方案就是按照通行的惯例,在没有找到相应资源时候,给浏览器响应一个错误码404,错误原因是"Not Found"。

通过重构,现在ClientHandler类中的响应部分代码也得到了简化,提高了代码的可读性和可维护性。HttpServletResponse类封装了HTTP响应的逻辑,使得ClientHandler更专注于业务处理部分。同时,为了更好地处理未找到资源的情况,我们返回了404错误页面,提高了用户体验。

这样的重构有助于进一步优化代码结构,提高代码的模块化和可维护性,使整体逻辑更加清晰。

3.1.4 HTTP响应状态码

RFC2616是HTTP/1.1协议的规范,其中定义了HTTP协议中的状态码。以下是RFC2616中定义的HTTP状态码及其含义。

1xx(信息性状态码):表示接收的请求正在处理。

  • 100 Continue:服务器已接收请求头部,并且客户端应继续发送请求的主体部分
  • 101 Switching Protocols:服务器已经理解了客户端的请求,并将通过升级协议来完成这个请求

2xx(成功状态码):表示请求已成功被服务器接收、理解、并接受。

  • 200 OK:请求已成功,请求所希望的响应头或数据体将随此响应返回
  • 201 Created:请求已经被实现,而且有一个新的资源已经依据请求的需要而建立
  • 202 Accepted:服务器已接受请求,但尚未处理
  • 204 No Content:服务器成功处理了请求,但没有返回任何内容

3xx(重定向状态码):表示需要客户端执行进一步的操作才能完成请求。

  • 301 Moved Permanently:请求的资源已被永久移动到新URI,将来的引用应使用新URI
  • 302 Found:请求的资源临时从不同的URI响应请求,将来的引用仍然应该使用原来的URI
  • 303 See Other:响应可以被找到在另一个URI,应使用GET方法来检索此资源
  • 304 Not Modified:请求的资源未被修改,客户端可以使用缓存的版本

4xx(客户端错误状态码):表示客户端在请求的过程中出错。

  • 400 Bad Request:服务器无法理解请求的格式,客户端不应该重复发送这个请求
  • 401 Unauthorized:请求需要用户验证,无法通过验证
  • 403 Forbidden:服务器已经理解请求,但是拒绝执行它
  • 404 Not Found:服务器无法找到请求的资源

5xx(服务器错误状态码):表示服务器在处理请求的过程中出错。

  • 500 Internal Server Error:服务器遇到了一个意外的情况,无法完成请求
  • 501 Not Implemented:服务器不支持客户端请求的功能
  • 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到了无效的响应
  • 503 Service Unavailable:服务器当前无法处理请求,可能是因为维护或过载

以上是RFC2616中定义的HTTP状态码及其含义,可以帮助开发者更好地理解HTTP协议中的错误码信息。

3.1.5 处理404错误

首先在 resources/static 文件夹中创建一个 404 错误的html文件 "404.html",该文件的HTML代码如下所示:

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>404</title>
</head>
<body>
    <p>404 文件没有找到!</p>
</body>
</html>

然后重构ClientHandler,处理404错误:先检查文件是否存在,如果文件存在,就发送文件;否则设置状态码"404",状态原因为"Not Found",并且设置发送404.html文件。代码如下所示:

java 复制代码
//1. 解析请求
HttpServletRequest request = new HttpServletRequest(socket);
HttpServletResponse response = new HttpServletResponse(socket);
String uri = request.getUri();

//2. 发送响应
//根据找到静态资源
//类加载路径:target/classes
File root = new File(
        ClientHandler.class.getClassLoader().getResource(".").toURI()
);
//定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)
File staticDir = new File(root,"static");
//定位target/classes/static目录中的文件
File file = new File(staticDir,uri);
//检查文件是否存在
if (file.isFile()){
    //正常发送资源
    response.setContentFile(file);
}else {
    //处理404错误
    response.setStatusCode(404);
    response.setStatusReason("Not Found");
    File file404 = new File(staticDir, "404.html");
    response.setContentFile(file404);;
}
//3. 发送响应
response.send();

重构后进行测试:请求一个不存在的资源,比如:http://localhost:8088/hi.html 得到如下结果:

3.1.6 重构处理请求过程

在对ClientHandler的请求和响应逻辑进行重构后,现在可以进一步重构ClientHandler的请求处理过程。将请求处理逻辑抽取到一个新的类DispatcherServlet中,该类作为请求处理器,包含了处理HTTP请求的逻辑。

  1. 抽取请求处理逻辑到一个新的类DispatcherServlet:一个请求处理器,包含了处理HTTP请求的逻辑。具体功能如下:
  • 根据请求中的URI定位到对应的静态资源文件,如果该文件存在则将其发送给浏览器
  • 如果请求的资源不存在,则设置HTTP响应的状态码为404,状态描述为"Not Found",并将静态资源文件404.html发送给浏览器

该类的静态初始化块中,通过类加载器获取到当前类所在的classpath目录,然后找到其中的static目录作为静态资源文件的根目录。包含属性:

  • root :代表当前classpath的根目录,是资源查找起始位置
  • staticDir :静态资源的位置,静态网页和图片都存储在这个位置
  1. 在service方法中,通过HttpServletRequest的getUri方法获取到请求的URI,然后在静态资源文件根目录下查找相应的文件,如果存在则将其发送给浏览器,如果不存在则发送静态资源文件404.html。

DispatcherServlet类的完整代码示意如下:

java 复制代码
/*封装请求处理逻辑 */
public class DispatcherServlet {
    private static File root;
    private static File staticDir;

    static {
        try {
            //根据找到静态资源
            //类加载路径:target/classes
            root = new File(
                    ClientHandler.class.getClassLoader().getResource(".").toURI()
            );
            //定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)
            staticDir = new File(root,"static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    public void service(HttpServletRequest request, HttpServletResponse response){
        String uri = request.getUri();
        //定位target/classes/static目录中的文件
        File file = new File(staticDir,uri);
        //检查文件是否存在
        if (file.isFile()){
            //正常发送资源
            response.setContentFile(file);
        }else {
            //处理404错误
            response.setStatusCode(404);
            response.setStatusReason("Not Found");
            File file404 = new File(staticDir, "404.html");
            response.setContentFile(file404);;
        }
    }
}
  1. 重构ClientHandler

重构后的请求处理线程ClientHandler就非常清爽:

java 复制代码
public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket clientSocket){
        socket = clientSocket;
    }

    @Override
    public void run() {
        try {
            //1. 解析请求
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);
            String uri = request.getUri();

            //2. 处理请求
            DispatcherServlet servlet = new DispatcherServlet();
            servlet.service(request, response);

            //3. 发送响应
            response.send();
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            //断开连接
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3.1.7 请求前的空行问题

在进行大量的测试时候,有可能出现解析请求时候出现了空行情况,收到空请求行后进行请求行解析就会出现异常:

其原因是HTTP协议中,允许客户端浏览器在HTTP请求前发送空行,也就是一个空行符(CRLF)的作用是分隔请求头和请求体,它表示请求头的结束。在请求头结束之后,如果请求中包含请求体,请求体将会跟在空行之后。由于存在空请求体的请求,所以存在请求行之间有空行的意外。

在 HTTP/1.1 规范中,如果服务器在开始读取一个消息时收到一个 CRLF,则应该忽略它,以确保服务器在遇到任何异常情况时都能正常工作。(可以参考:RFC2616 4.1 Message Types)

3.1.8 检查请求行

为解决请求前的空行问题,需要在解析请求行的时候,忽略空行。然后再利用正则表达式检查请求行是否是合乎HTTP协议的标准,进一步增强程序的可靠性。

可以使用AI工具帮助生成正则表达式。

一个检查请求行正确的正则表达式如下:

java 复制代码
^(GET|POST|PUT|DELETE|HEAD|OPTIONS) ([^?#\s]+)(\?[^#\s]*)? (HTTP\/1\.0|HTTP\/1\.1)$

这个正则表达式匹配了HTTP请求行的四个部分:请求方法、请求URL、请求参数、HTTP协议版本。

  • ^:表示字符串的开始
  • (GET|POST|PUT|DELETE|HEAD|OPTIONS):匹配HTTP请求的方法,这里使用了分组和|操作符表示多个可能的方法
  • ([^?#\s]+):匹配请求URI,使用了非贪婪的正则表达式表示法,不包含URI中可能存在的参数和锚点
  • (\?[^#\s]*)?:匹配请求URI中的参数,使用了可选分组,匹配以?开头的参数部分,可以不出现
  • (HTTP\/1\.0|HTTP\/1\.1):匹配HTTP协议的版本号,同样使用了分组和|操作符

其中,\是转义字符,用于匹配特殊字符。正则表达式中的 . 和 | 都是特殊字符,需要用\进行转义。

先添加错误请求的自定义异常 BadRequestException:

java 复制代码
/* 错误请求格式异常 */
public class BadRequestException extends Exception{
    public BadRequestException() {
    }

    public BadRequestException(String message) {
        super(message);
    }

    public BadRequestException(String message, Throwable cause) {
        super(message, cause);
    }

    public BadRequestException(Throwable cause) {
        super(cause);
    }

    public BadRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

然后重构请求行解析方法:

java 复制代码
/**
 * 解析请求行方法
 * @throws IOException 网络出现异常
 * @throws BadRequestException 请求行格式错误
 */
private void parseRequestLine() throws IOException, BadRequestException {
    String requestLine = readLine();
        //根据HTTP协议描述,requestLine 有可能是空行!
        int n = 0;
        while (requestLine.isEmpty()){
            //跳过
            requestLine = readLine();
            if (n++ == 5){
                throw new BadRequestException("过多的空请求行!");
            }
        }
    String regex = "^(GET|POST|PUT|DELETE|HEAD|OPTIONS) ([^?#\\s]+)(\\?[^#\\s]*)? (HTTP\\/1\\.0|HTTP\\/1\\.1)$";
    if (! requestLine.matches(regex)){
        throw new BadRequestException("错误的请求行格式");
    }
    String[] parts = requestLine.split("\\s");
    method = parts[0];
    uri = parts[1];
    protocol = parts[2];
    System.out.println("解析请求行:"+requestLine);
    System.out.println("method:"+method);
    System.out.println("uri:"+uri);
    System.out.println("protocol:"+protocol);
}

通过重构,现在ClientHandler类中的请求处理过程变得非常清晰简洁。我们将请求处理逻辑抽取到了DispatcherServlet类中,使得ClientHandler更专注于处理连接和调用请求处理器的功能。这样的设计提高了代码的模块化和可维护性,使整体结构更清晰,更易于后续的开发和维护。

3.2 单例模式

3.2.1 设计模式与单例模式

设计模式是针对面向对象编程中常见的问题和场景,提出的一套经过反复实践验证的解决方案的方法论,它描述了一组经过测试和证明的解决方案,可以用来解决面向对象编程中的各种问题。

单例模式是一种常用的设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例。在单例模式中,通常将该类的构造函数私有化,防止外部直接创建实例,而通过一个静态方法或者变量来获取唯一的实例。

单例模式可以避免在系统中出现多个相同的对象,减小系统开销,并且方便对这个唯一实例进行统一的管理和控制。在需要频繁创建和销毁对象的场景下,采用单例模式可以提高系统的性能和可维护性。

在实际开发中,单例模式的应用非常广泛,例如,线程池、数据库连接池、日志系统等等都可以采用单例模式来保证全局唯一性和统一管理。但是,在使用单例模式时也需要注意一些问题,例如线程安全性、延迟加载等等。

3.2.2 使用单例模式重构请求处理DispatcherServlet

使用单例模式重构请求处理DispatcherServlet可以优化资源的创建和提高软件效能。在Java中,创建对象的过程涉及一定的内存和时间开销,如果可以减少对象的创建次数,可以提升程序性能。在这里,我们可以使用饿汉单例模式来确保DispatcherServlet在整个应用程序中只有一个实例。

首先,我们需要将DispatcherServlet类设计为单例模式。饿汉单例模式的实现比较简单,可以在类加载时就创建唯一的实例对象,保证了线程安全性。

接下来,我们需要在ClientHandler类中使用DispatcherServlet的单例实例。

通过以上重构,我们将DispatcherServlet类设计为了饿汉单例模式,确保整个应用程序中只有一个DispatcherServlet实例。同时,在ClientHandler类中使用DispatcherServlet.getInstance()来获取该单例实例。

以下是将DispatcherServlet重构为饿汉单例模式的代码:

java 复制代码
public class DispatcherServlet {
    // 1. 将构造方法私有化,防止外部通过new创建实例
    private DispatcherServlet(){
    }
    //2. 定义一个静态变量来保存实例,并进行初始化
    private static DispatcherServlet instance = new DispatcherServlet();
    // 3. 提供一个公有的静态方法来获取实例
    public static DispatcherServlet getInstance() {
        return instance;
    }
    // 略去 请求处理代码 ...
}

在上面的代码中,我们将DispatcherServlet的构造函数设置为私有,这样外部就无法通过new DispatcherServlet()来实例化对象。同时,我们在类加载时就创建了一个唯一的DispatcherServlet实例,并通过静态方法getInstance()来获取该实例。

接下来,我们需要在ClientHandler类中使用DispatcherServlet的单例实例:

java 复制代码
DispatcherServlet servlet = DispatcherServlet.getInstance();

通过以上重构,我们将DispatcherServlet类设计为了饿汉单例模式,确保整个应用程序中只有一个DispatcherServlet实例。

相关推荐
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring
数据小小爬虫4 小时前
如何用Java爬虫“偷窥”淘宝商品类目API的返回值
java·爬虫·php
暮春二十四4 小时前
关于用postman调用接口成功但是使用Java代码调用却失败的问题
java·测试工具·postman