第3章 WebServer重构

news2024/9/23 1:38:00

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请求头的所有内容,以键值对的形式存储

        2. 定义属性访问方法

  • public String getMethod():返回HTTP请求的方法
  • public String getUri():返回HTTP请求的URI
  • public String getProtocol():返回HTTP请求使用的协议

        3. 定义方法private void parseRequestLine(),用于解析HTTP请求的请求行,包括请求方法、URI和协议版本号,将解析结果输出到控制台,便于后续调试。

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

  • private void parseHeaders():解析HTTP请求头部的所有内容,以键值对的形式存储。
  • public String getHeader(String name):根据请求头的名称返回请求头的值

        5. 定义构造函数public HttpServletRequest(Socket socket):接受一个Socket对象作为参数,通过解析Socket中的输入流,初始化该类的成员变量。并定义方法public String readLine(),用于从Socket的输入流中读取一行数据并返回。

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

/*封装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);
    }
}

        6. 重构ClientHandler类,将解析请求部分替换为HttpServletRequest:

//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实例变量。

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

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

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

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

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

/*封装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类替换响应过程:

//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();

        6. 重构后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代码如下所示:

<!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文件。代码如下所示:

//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 :静态资源的位置,静态网页和图片都存储在这个位置

        2. 在service方法中,通过HttpServletRequest的getUri方法获取到请求的URI,然后在静态资源文件根目录下查找相应的文件,如果存在则将其发送给浏览器,如果不存在则发送静态资源文件404.html。

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

/*封装请求处理逻辑 */
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);;
        }
    }
}

        3. 重构ClientHandler

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

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工具帮助生成正则表达式。

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

^(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:

/* 错误请求格式异常 */
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);
    }
}

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

/**
 * 解析请求行方法
 * @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重构为饿汉单例模式的代码:

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的单例实例:

DispatcherServlet servlet = DispatcherServlet.getInstance();

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

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

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

相关文章

18.Blender 渲染工程、打光方法及HDR贴图导入

HDR环境 如何导入Blender的HDR环境图 找到材质球信息 在右上角&#xff0c;点击箭头&#xff0c;展开详细部分 点击材质球&#xff0c;会出现下面一列材质球&#xff0c;将鼠标拖到第二个材质球&#xff0c;会显示信息 courtyard.exr 右上角打开已渲染模式 左边这里选择世界…

【Elasticsearch<四>✈️✈️】SpringBoot 项目整合 Elasticsearch

目录 &#x1f378;前言 &#x1f37b;一、Elasticsearch 本地环境启动 &#x1f37a;二、SpringBoot 项目整合 Elasticsearch 2.1 引入 ES 依赖 2.2 配置 ES 属性 2.3 创建实体类 2.4 操作 ES 的工具类 2.5 操作 ES 的业务层 &#x1f379;三、接口测试 3.1 编写测试类 3…

【信息安全】密码学

信息验证遇到的问题Message Authentication In the context of communications across a network, the following attacks can be identified. 泄密Disclosure 流量分析Traffic analysis 伪装Masquerade Content modification Sequence modification Time modification …

一、写给Android开发者之harmony入门

一、创建新项目 对比 android-studio&#xff1a;ability类似安卓activity ability分为两种类型(Stage模型) UIAbility和Extensionability&#xff08;提供系统服务和后台任务&#xff09; 启动模式 1、 singleton启动模式&#xff1a;单例 2、 multiton启动模式&#xff1…

学习Rust的第29天: cat in Rust

今天即将是这个系列的最后一次内容&#xff0c;我们正在catRust 中从 GNU 核心实用程序进行重建。cat用于将文件内容打印到STDOUT.听起来很容易构建&#xff0c;所以让我们开始吧。 GitHub 存储库&#xff1a;GitHub - shafinmurani/gnu-core-utils-rust 伪代码 function read(…

Django实验(远程访问+图片显示)

众所周知&#xff0c;Python除了不能生孩子什么都会。Python也是可以做web服务的。 Python做web有一个重点优势是&#xff1a;做一个快速的AI Demo。 第一步&#xff1a;安装一个版本5.0以上django 第二步&#xff1a;构建咱们的Django工程&#xff0c;我取名为BBQ django-adm…

基于 Ubuntu22.04 安装 SSH 服务

文章目录 一、Ubuntu22.04 安装 SSH 服务二、配置 OpenSSH&#xff08;安全性&#xff09;1. 更改 OpenSSH 端口2. 限制使用 SSH 登录尝试次数3. 禁止 SSH 以 root 身份连接 三、设置防火墙&#xff08;UFW&#xff09;锁定 SSH四、远程终端软件通过 SSH 连接 Ubuntu22.041. 远…

js api part4

其他事件 页面加载事件 外部资源&#xff08;如图片、外联CSS和JavaScript等&#xff09;加载完毕时触发的事件 原因&#xff1a;有些时候需要等页面资源全部处理完了做一些事情&#xff0c;老代码喜欢把 script 写在 head 中&#xff0c;这时候直接找 dom 元素找不到。 事件…

实测好评!微信自动回复消息神器!高效沟通拿捏住!

随着企业规模的扩大和客户数量的增加&#xff0c;有效管理和回复每一条消息变得越来越具有挑战性。今天&#xff0c;就给大家分享一个高效的自动回复消息神器——个微管理系统&#xff0c;让你能够轻松应对各种沟通需求。 1、自动通过好友&#xff0c;提高沟通效率 每当有新的…

AI神助攻!小白也能制作自动重命名工具~

我们平时从网上下载一些文件&#xff0c;文件名很多都是一大串字母和数字&#xff0c;不打开看看&#xff0c;根本不知道里面是什么内容。 我想能不能做个工具&#xff0c;把我们一个文件夹下面的所有word、excel、ppt、pdf文件重命名为文件内容的第一行。 我们有些朋友可能不会…

PHP基于B/S版 医院不良事件管理系统源码vscode+laravel8医院如何加强不良事件上报系统的管理 AEMS系统源码

PHP基于B/S版 医院不良事件管理系统源码vscodelaravel8医院如何加强不良事件上报系统的管理 AEMS系统源码 医院安全&#xff08;不良&#xff09;事件管理AEMS系统AEMS采用无责的、自愿的填报不良事件方式&#xff0c;有效地减轻医护人员的思想压力&#xff0c;实现以事件为主要…

数据分析从入门到精通 2.pandas修真之前戏基础

从爱上自己那天起&#xff0c;人生才真正开始 —— 24.5.6 为什么学习pandas numpy已经可以帮助我们进行数据的处理了&#xff0c;那么学习pandas的目的是什么呢? numpy能够帮助我们处理的是数值型的数据&#xff0c;当然在数据分析中除了数值型的数据还有好多其他类型…

笔试强训-day17_T2 十字爆破

一、题目链接 十字爆破 二、题目描述 牛牛在玩一个游戏&#xff1a; 一共有n行m列共nm个方格&#xff0c;每个方格中有一个整数。 牛牛选择一个方格&#xff0c;可以得到和这个方格同行、同列的所有数之和的得分。 例如&#xff1a;对于一个22的方格&#xff1a; 1 2 3 4 牛牛…

用js代码实现贪吃蛇小游戏

js已经学了大部分了&#xff0c;现在就利用我所学的js知识试试做贪吃蛇小游戏吧 以下部分相关图片以及思路笔记均出自渡一陈老师的视频 首先制作简单的静态页面&#xff0c;添加贪吃蛇移动的背景和相关图片&#xff0c;比如开始游戏等等 将各个功能均封装在函数中&#xff0…

大数据技术主要学什么,有哪些课程

大数据技术是指在海量数据的环境下&#xff0c;采集、存储、处理、分析和管理数据的一系列技术与方法。随着互联网、物联网以及各种智能设备的普及&#xff0c;数据量呈爆炸性增长&#xff0c;传统数据处理手段已难以应对&#xff0c;因此大数据技术应运而生&#xff0c;旨在从…

包管理工具npm、cnpm、yarn、NVM

[包]英文单词是package,代表了一组特定功能的源码集合 包管理工具&#xff1a; 管理[包]的应用软件,可以对[包]进行下载安装,更新,删除,上传等操作借助包管理工具,可以快速开发项目,提升开发效率 包管理工具是一个通用的概念,很多编程语言都有包管理工具,所以掌握好包管理工具非…

供应链|经典论文解读:(s,S) 策略在动态库存下的最优性

文章考虑了具有订购成本&#xff08;由单位成本加上重新订购成本组成&#xff09;的动态库存问题。具体而言&#xff0c;对于每个时期&#xff0c;系统在中期开始是做出一系列采购决策——这些采购有助于库存的积累&#xff0c;并在随后的周期被需求所消耗。每时期系统会产生各…

开源15T tokens!HuggingFace放出规模最大、质量最高预训练数据集 | 最新快讯

新智元报道 编辑&#xff1a;LRS FineWeb 是一个高质量的预训练数据集&#xff0c;包含 15T 个 tokens&#xff0c;主要包含英语文本&#xff1b;消融实验证明了 FineWeb 数据集的质量要高于其他开源数据集&#xff1b;数据清洗脚本也已开源。 Meta 最近开源的 Llama 3 模型再次…

如何根据IP获取国家省份城市名称PHP免费版

最近项目遇到需要根据IP获取用户国家功能需求&#xff0c;网上找了一下&#xff0c;很多API接口都需要付费&#xff0c;考虑为公司节约成本&#xff0c;就取找找有没有开源的 github 上面那个包含多种语言&#xff0c;下面这个只有php&#xff0c;用法很简单 $ip 114.114.114…

QT7_视频知识点笔记_1_ 基础知识(帮助文档),窗口(内存回收机制),信号槽(传参),Lambda表达式

1.QT基础 QT是一个框架&#xff0c;不用像C语言自己从底层写&#xff0c;需要的功能可以先看是否QT库中有直接可使用的 帮助文档的使用&#xff1a;F1跳入帮助文档&#xff0c; QT中常用的类&#xff1a;比如QPushbutton&#xff0c;查看帮助文档则可知道对应的函数和解决方…