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

        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实例。

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

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

相关文章

快速掌握Redis优化要点,告别性能瓶颈!

大家好!我是小米,今天和大家分享一下在Redis中如何进行优化,以提升系统性能。Redis作为一种流行的内存数据库,因其高性能、高可用和数据持久性而受到广泛应用。然而,在实际应用中,我们仍需对Redis进行优化,以满足各种业务需求。接下来,我将从读写方式、KV size、Key数量…

VALSE 2024主旨报告内容解析:以深度学习框架为牵引促进自主AI生态发展

2024年视觉与学习青年学者研讨会&#xff08;VALSE 2024&#xff09;于5月5日到7日在重庆悦来国际会议中心举行。本公众号将全方位地对会议的热点进行报道&#xff0c;方便广大读者跟踪和了解人工智能的前沿理论和技术。欢迎广大读者对文章进行关注、阅读和转发。文章是对报告人…

探秘Flex布局下子元素宽度超出的那些烦心事

嘿&#xff0c;小伙伴们&#xff01;你们有没有遇到过用Flex布局的时候&#xff0c;子元素的宽度莫名其妙地超出了父元素的情况&#xff1f;别着急&#xff0c;今天我就来给大家揭秘这个问题的来龙去脉&#xff0c;以及一些解决方案。让我们一起来深入探讨&#xff01; 发现问…

【Gaea+UE5】创建基本的大型世界场景

目录 效果 步骤 一、在Gaea中生成地形 二、确定导出的地形规模 三、在UE中创建地形 四、验证UE创建的地形规模是否正确 五、使用M4自动地形材质 效果 步骤 一、在Gaea中生成地形 1. 打开Gaea官网下载软件 2. 打开Gaea软件&#xff0c;我们可以选择一个预设的山体 创…

Git === Git概述 Git安装

第1章 Git概述 Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。 Git易于学习&#xff0c;占地面积小&#xff0c;性能极快。 它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。其性能优于Subversion…

汇凯金业:通货膨胀对能源行业有何影响

通货膨胀对能源行业有几方面的影响&#xff0c;具体取决于通货膨胀的原因、规模以及持续时间。以下是一些可能的效应&#xff1a; 成本增加&#xff1a;通货膨胀导致能源行业的运营成本上升。这包括原材料、设备、维护和人力成本。如果企业不能完全将成本转嫁给消费者&#xf…

Pytorch入门实战 P09-YOLOv5里面的Backbone模块搭建网络

目录 1、YOLOv5的模型图。 2、BackBone简单介绍。 3、YOLOv5的Backbone文件。 4、YOLOv5Backbone的code部分 5、完整的code部分 6、结果展示 &#xff08;1&#xff09;Adam优化器 &#xff08;2&#xff09;SGD优化器 &#x1f368; 本文为&#x1f517;365天深度学习…

linux系统下产生Segmentation fault 与 Segmentation fault (core dumped)!!!

最近在学习的过程中&#xff0c;遇到了Segment fault&#xff08;段错误&#xff09;的问题&#xff0c;经过一番查找资料&#xff0c;学到了一些相关知识&#xff0c;这里做一个梳理&#xff0c;以防以后在遇到类似的问题&#xff0c;并且希望能够帮助到大家一丝丝&#xff01…

华为AI全栈生态布局:中国科技巨头加速创新

华为AI芯片生态全栈深度分析 2024 一、引言 1.1 华为AI芯片发展背景&#xff1a; 华为&#xff0c;通信和消费电子巨头&#xff0c;以其技术创新和远见著称。2013年&#xff0c;华为率先布局人工智能&#xff08;AI&#xff09;&#xff0c;并专注于全栈AI解决方案的开发。华…

骨传导耳机哪个品牌值得入手?精选五款高性能骨传导耳机,闭眼入都不踩雷!

随着健康生活的日益普及&#xff0c;运动健身逐渐成为人们生活中的重要组成部分。在这一背景下&#xff0c;骨传导耳机作为一种新型蓝牙耳机&#xff0c;凭借其不堵塞耳道、防水性能强等特性&#xff0c;受到了广大运动爱好者的喜爱。然而&#xff0c;骨传导耳机的热销也吸引了…

一次性邮箱API发送邮件的方法?如何配置?

一次性邮箱API发送邮件有哪些注意事项&#xff1f;怎么安全发信&#xff1f; 随着网络安全问题的日益凸显&#xff0c;如何安全、高效地发送邮件成为了一个亟待解决的问题。一次性邮箱API的出现&#xff0c;为我们提供了一种新的解决方案。那么&#xff0c;如何使用一次性邮箱…

白酒:白酒香型的国际化推广与市场接受度分析

云仓酒庄的豪迈白酒一直有在白酒香型的国际化推广。随着中国白酒市场的不断扩大和国际化的趋势&#xff0c;了解白酒香型的国际接受度和推广策略对于酒厂和整个行业都具有重要意义。 首先&#xff0c;国际化推广需要深入了解国际市场的需求和消费者偏好。不同国家和地区的消费者…

长难句打卡5.7

In December 2010 America’s Federal Trade Commission (FTC) proposed adding a “do not track” (DNT) option to Internet browsers, so that users could tell advertisers that they did not want to be followed. 2010年12月&#xff0c;美国美国联邦贸易委员会(FTC)提…

020、Python+fastapi,第一个Python项目走向第20步:ubuntu 24.04 docker 安装mysql8集群+redis集群(一)

系列文章 pythonvue3fastapiai 学习_浪淘沙jkp的博客-CSDN博客https://blog.csdn.net/jiangkp/category_12623996.html 前言 docker安装起来比较方便&#xff0c;不影响系统整体&#xff0c;和前面虚拟环境有异曲同工之妙&#xff0c;今天把老笔记本T400拿出来装了个ubuntu24…

Spring AOP(3)

目录 Spring AOP原理 代理模式 代理模式中的主要角色 静态代理 动态代理 总结:面试题 什么是AOP? Spring AOP实现的方式有哪些? Spring AOP实现原理 Spring使用的是哪种代理方式? JDK和CGLIB动态代理的区别? Spring AOP原理 代理模式 代理模式, 也叫委托模式. …

CUDA C编程:第一个程序 向量相加

我的电脑没有装CUDA&#xff0c;所以使用租了带GPU的云服务器&#xff0c;然后使用vscode SSH远程连接云服务器。云GPU使用的是智星云&#xff0c;0.8元/h。 智星云 可以使用nvcc --version查看系统中安装的CUDA版本。 然后写第一个CUDA程序&#xff0c;两个向量相加结果给到…

绝地求生:季后赛名额确定!NH战队总积分榜排名第一!

2024年5月5日&#xff0c;PCL春季赛常规赛第五阶段第三天比赛结束&#xff0c;今天打完春季赛常规赛结束&#xff0c;16个战队进入季后赛的名额已确定。NH战队总积分506分&#xff0c;总积分榜排名第一&#xff01;&#xff01;NH战队也是唯一一支总积分超过500分的队伍。今天最…

语音识别之其他谱图

⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容&#xff0c; 欢迎&#x1f44f;关注&#x1f440;【文末】我的个人微信公众号&#xf…

护眼灯有没有护眼的效果?一键查看这五大护眼效果极佳的护眼台灯

在数字时代&#xff0c;护眼灯已成为保护视力的重要工具。但消费者常问&#xff1a;护眼灯有没有护眼的效果&#xff1f;挑选到技术过关的护眼台灯是能够很好地起到护眼效果的。本文将并重点介绍五款具有卓越护眼功能的台灯。这些精选灯具不仅在照明效果上表现出色&#xff0c;…

leetcode-缺失的第一个正整数-96

题目要求 思路 1.这里的题目要求刚好符合map和unordered_map 2.创建一个对应map把元素添加进去&#xff0c;用map.find(res)进行查找&#xff0c;如果存在返回指向该元素的迭代器&#xff0c;否则返回map::end()。 代码实现 class Solution { public:int minNumberDisappeare…