【吃透Java手写】4-Tomcat-简易版

news2024/11/24 10:03:56

【吃透Java手写】Tomcat-简易版-源码解析

  • 1 准备工作
    • 1.1 引入依赖
    • 1.2 创建一个Tomcat的启动类
  • 2 线程池技术回顾
    • 2.1 线程池的使用流程
    • 2.2 线程池的参数
      • 2.2.1 任务队列(workQueue)
      • 2.2.2 线程工厂(threadFactory)
      • 2.2.3 拒绝策略(handler)
    • 2.3 功能线程池
      • 2.3.1 定长线程池(FixedThreadPool)
      • 2.3.2 定时线程池(ScheduledThreadPool )
      • 2.3.3 可缓存线程池(CachedThreadPool)
      • 2.3.4 单线程化线程池(SingleThreadExecutor)
      • 2.3.5 对比
  • 3 Tomcat逻辑
    • 3.1 请求
      • 3.1.1 单次请求
      • 3.1.2 多次请求
      • 3.1.3 线程池处理请求
    • 3.2 处理socket连接
      • 3.2.1 http协议格式
      • 3.2.2 具体解析
      • 3.2.3 Request
    • 3.3 请求类Request
    • 3.4 响应类Response
      • 3.4.1 请求体输出流ResponseOutputStream
    • 3.5 servlet SJBServlet
    • 3.6 测试
    • 3.7 Tomcat的部署应用
      • 3.7.1 @WebServlet 注解 和 web.xml 的区别
      • 3.7.2 目录存放
      • 3.7.3 部署
      • 3.7.4 部署测试


1 准备工作

1.1 引入依赖

<dependencies>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.0.1</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>3.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

1.2 创建一个Tomcat的启动类

创建com.sjb.Tomcat

public class Tomcat {
    public void start(){

    }
    public static void main(String[] args) {
        Tomcat tomcatApplication = new Tomcat();
        tomcatApplication.start();
    }
}

2 线程池技术回顾

线程池的真正实现类是 ThreadPoolExecutor

2.1 线程池的使用流程

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

2.2 线程池的参数

主要参数:

  • corePoolSize(必需)

    核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • maximumPoolSize(必需)

    线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞

  • keepAliveTime(必需)

    线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。

  • unit(必需)

    指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

  • workQueue(必需)

    任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。

  • threadFactory(可选)

    线程工厂。用于指定为线程池创建新线程的方式。

  • handler(可选)

    拒绝策略。当达到最大线程数时需要执行的饱和策略。

2.2.1 任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE。
  3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  4. DelayQueue: 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  5. SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  6. LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)。
  7. LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。

注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。

2.2.2 线程工厂(threadFactory)

线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂

2.2.3 拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:

  1. AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  2. CallerRunsPolicy:由调用线程处理该任务。
  3. DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

2.3 功能线程池

Executors已经为我们封装好了 4 种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)
  • 定时线程池(ScheduledThreadPool )
  • 可缓存线程池(CachedThreadPool)
  • 单线程化线程池(SingleThreadExecutor)

2.3.1 定长线程池(FixedThreadPool)

源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:控制线程最大并发数。

使用示例:

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);

2.3.2 定时线程池(ScheduledThreadPool )

源码:

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
 
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}
  • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
  • 应用场景:执行定时或周期性的任务。
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

2.3.3 可缓存线程池(CachedThreadPool)

源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • 特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
  • 应用场景:执行大量、耗时少的任务。
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);

2.3.4 单线程化线程池(SingleThreadExecutor)

源码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • 特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);

2.3.5 对比

在这里插入图片描述

3 Tomcat逻辑

3.1 请求

3.1.1 单次请求

在com.sjb.Tomcat#start

public class Tomcat {
    public void start(){
        //socket连接TCP
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            Socket socket = serverSocket.accept();
            processSocket(socket);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }
    private void processSocket(Socket socket) {
        //处理socket请求
    }

    public static void main(String[] args) {
        Tomcat tomcatApplication = new Tomcat();
        tomcatApplication.start();
    }
}

使用了 ServerSocket 类来监听8080端口上的连接。

通过serverSocket.accept()阻塞监听8080端口,一直等待直到有一个客户端连接请求到达。一旦有连接请求到达,accept() 方法会返回一个新的 Socket 对象,该对象表示服务器和客户端之间建立的连接。然后你就可以使用这个 Socket 对象来进行通信,发送和接收数据。

每当有一个连接到达,它会调用 processSocket() 方法来处理该连接。

3.1.2 多次请求

但是这个有个只处理单次请求,我们需要加上一个while来不断地进行阻塞监听

try {
    ServerSocket serverSocket = new ServerSocket(8080);
    while(true){
        Socket socket = serverSocket.accept();
        processSocket(socket);
    }
} catch (IOException e) {
    throw new RuntimeException(e);
}

但是这个是线性的,一次只能处理一个

3.1.3 线程池处理请求

引入线程池,提升并行处理能力

public class Tomcat {
    public void start(){
        //socket连接TCP
        try {
            ExecutorService executorService = Executors.newFixedThreadPool(20);
            ServerSocket serverSocket = new ServerSocket(8080);
            while(true){
                Socket socket = serverSocket.accept();
                executorService.execute(new SocketProcessor(socket));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) {
        Tomcat tomcatApplication = new Tomcat();
        tomcatApplication.start();
    }
}

创建com.sjb.SocketProcessor实现Runnable

public class SocketProcessor implements Runnable{
    private Socket socket;

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

    @Override
    public void run() {
        processSocket(socket);
    }

    private void processSocket(Socket socket) {
        //处理socket
    }
}

3.2 处理socket连接

3.2.1 http协议格式

在这里插入图片描述

tomcat需要按照这个格式进行解析,这里先把他全部输出就不解析了。

在com.sjb.SocketProcessor#processSocket中

private void processSocket(Socket socket) {
    //处理socket
    try {
        InputStream inputStream = socket.getInputStream();
        byte[] bytes = new byte[1024];
        //循环读取数据
        while (true){
            int read = inputStream.read(bytes);
            if(read == -1){
                break;
            }
            System.out.println(new String(bytes,0,read));
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

访问http://localhost:8080/,输出

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Cookie: Idea-7b055fc6=c09dc312-ccaa-43d4-bf1f-95a39406b2e6; username-localhost-60524="2|1:0|10:1713800933|24:username-localhost-60524|196:eyJ1c2VybmFtZSI6ICJjMTdjZmFhYjA0MGQ0Njg3YWFjNzZiYzc1MmQ3Zjc0ZCIsICJuYW1lIjogIkFub255bW91cyBUaHlvbmUiLCAiZGlzcGxheV9uYW1lIjogIkFub255bW91cyBUaHlvbmUiLCAiaW5pdGlhbHMiOiAiQVQiLCAiY29sb3IiOiBudWxsfQ==|d3
42be023b8e3ab274c8019dd4a41dc7f3102301b8faf7ea83435c06dd9b614b"; _xsrf=2|0e634e08|aaad0368070a9f2267481456f73e2dd6|1713800933; username-localhost-61085=2|1:0|10:1713801376|24:username-localhost-61085|204:eyJ1c2VybmFtZSI6ICJhM2IzOTQwNjhjYzM0OWM4OTYyYzUxODQ2Y2IwNzU1YiIsICJuYW1lIjogIkFub255bW91cyBDYWxsaXJyaG9lIiwgImRpc3BsYXlfbmFtZSI6ICJBbm9ueW1vdXMgQ2FsbGlycmhvZSIsICJpbml0aWFscyI6ICJBQyIsICJjb2xvciI6IG51bGx9|e49adff6d260df7ee49848807369b565e82e36906318b9546796c968f21a9361; username-localhost-61138="2|1:0|10:1713801443|24:username-localhost-61138|200:eyJ1c2VybmFtZSI6ICI4YzVkOWRkYTBmNGE0YTk1YjQ0MjFkMzYwZDRmNTFhMyIsICJuYW1lIjogIkFub255bW91cyBMeXNpdGhlYSIsICJkaXNwbGF5X25hbWUiOiAiQW5vbnltb3VzIEx5c2l0aGVhIiwgImluaXRpYWxzIjogIkFMIiwgImNvbG9yIjogbnVsbH0=|587e609fe7fc575e84763d39c6fd8b9feb8cdd0da05046d03dec57ba58307aef"

3.2.2 具体解析

我们需要获得method、url、protocl来创建Request对象

访问http://localhost:8080/abc

//处理socket
try {
    InputStream inputStream = socket.getInputStream();
    //解析请求头,获得method、url、protocol
    byte[] bytes = new byte[1024];
    int read = inputStream.read(bytes);
    String request = new String(bytes, 0, read);
    String[] split = request.split("\r\n");
    String[] split1 = split[0].split(" ");
    String method = split1[0];
    String url = split1[1];
    String protocol = split1[2];
    System.out.println("method: " + method);
    System.out.println("url: " + url);
    System.out.println("protocol: " + protocol);
} catch (IOException e) {
    throw new RuntimeException(e);
}

输出

method: GET
url: /abc
protocol: HTTP/1.1

请求头和请求体同理

3.2.3 Request

创建com.sjb.Request

public class Request {
    private String method;
    private String url;
    private String protocol;
    public Request(String method, String url, String protocol) {
        this.method = method;
        this.url = url;
        this.protocol = protocol;
    }
    public String getMethod() {
        return method;
    }
    public String getUrl() {
        return url;
    }
    public String getProtocol() {
        return protocol;
    }
}

在com.sjb.SocketProcessor#processSocket中创建Request对象,再进行之后的匹配servlet之后执行对应的doGet方法、doPost方法等等

//处理socket
try {
    InputStream inputStream = socket.getInputStream();
    //解析请求头,获得method、url、protocol
    byte[] bytes = new byte[1024];
    int read = inputStream.read(bytes);
    String request = new String(bytes, 0, read);
    String[] split = request.split("\r\n");
    String[] split1 = split[0].split(" ");
    String method = split1[0];
    String url = split1[1];
    String protocol = split1[2];
    Request tomcatrequest = new Request(method, url, protocol);
    //根据url找到对应的servlet
    
} catch (IOException e) {
    throw new RuntimeException(e);
}

3.3 请求类Request

Request类需要实现接口HttpServletResponse,里面有很多方法,我们避免麻烦创建一个com.sjb.AbstractHttpServletResponse类实现接口,我们的Request类只需要继承AbstractHttpServletResponse即可

创建com.sjb.AbstractHttpServletResponse

public class AbstractHttpServletRequest implements HttpServletRequest {
    @Override
    public String getAuthType() {
        return null;
    }

    @Override
    public Cookie[] getCookies() {
        return new Cookie[0];
    }
    .................

创建com.sjb.Request

public class Request extends AbstractHttpServletRequest {
    private String method;
    private String url;
    private String protocol;
    private Socket socket;

    public Socket getSocket() {
        return socket;
    }
    public Request(String method, String url, String protocol, Socket socket) {
        this.method = method;
        this.url = url;
        this.protocol = protocol;
        this.socket = socket;
    }
    public String getMethod() {
        return method;
    }
    public StringBuffer getRequestURL() {
        return new StringBuffer(url);
    }
    public String getProtocol() {
        return protocol;
    }
}

public Request(String method, String url, String protocol, Socket socket)一个socket对应一个Request对应一个response

3.4 响应类Response

与3.3类似

创建com.sjb.AbstractHttpServletResponse

public class AbstractHttpServletResponse implements HttpServletResponse {
    @Override
    public void addCookie(Cookie cookie) {

    }

    @Override
    public boolean containsHeader(String s) {
        return false;
    }
    ...................

创建com.sjb.Response

public class Response extends AbstractHttpServletResponse {
    private String message="OK";
    private int status=200;
    private Map<String,String> headers = new HashMap<>();
    private Request request;
    private OutputStream socketOutputStream;
    private ResponseOutputStream responseOutputStream=new ResponseOutputStream();

    public Response(Request request) {
        this.request = request;
        try {
            this.socketOutputStream = request.getSocket().getOutputStream();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public void setStatus(int status, String message) {
        this.status = status;
        this.message = message;
    }
    @Override
    public int getStatus() {
        return status;
    }
    @Override
    public void addHeader(String s, String s1) {
        headers.put(s, s1);
    }
    @Override
    public ResponseOutputStream getOutputStream() throws IOException {
        return responseOutputStream;
    }
    public void complete() {
        //发送响应
        sendResponseLine();
        sendResponseHeader();
        sendResponseBody();
    }
    private void sendResponseBody() {
        try {
            byte[] body = getOutputStream().getBody();
            int pos = getOutputStream().getPos();
            socketOutputStream.write(body, 0, pos);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private void sendResponseHeader() {
        try {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                socketOutputStream.write(key.getBytes());
                socketOutputStream.write(": ".getBytes());
                socketOutputStream.write(value.getBytes());
                socketOutputStream.write("\r\n".getBytes());
            }
            socketOutputStream.write("\r\n".getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        
    }
    private void sendResponseLine() {
        try {
            socketOutputStream.write(request.getProtocol().getBytes());
            socketOutputStream.write(" ".getBytes());
            socketOutputStream.write(String.valueOf(status).getBytes());
            socketOutputStream.write(" ".getBytes());
            socketOutputStream.write(message.getBytes());
            socketOutputStream.write("\r\n".getBytes());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }    
    }
}

属性:

  • int status:状态码
  • String message:说明
  • Map<String,String> header:存储响应头,例如一些长度、编码信息等
  • Request request:一个response对应一个request
  • OutputStream socketOutputStream:socket的整个输出流
  • ResponseOutputStream responseOutputStream:响应体的输出流,因为响应体可能有很多,所以需要用一个输出流进行缓存,并且如果响应失败的话,也只有响应行和响应头返回,响应体不返回。

private ResponseOutputStream responseOutputStream=new ResponseOutputStream();一开始就创建好响应响应体的输出流,保证public ResponseOutputStream getOutputStream()拿到的都是同一个响应体的输出流

3.4.1 请求体输出流ResponseOutputStream

因为响应体可能有很多,所以需要用一个输出流进行缓存,并且如果响应失败的话,也只有响应行和响应头返回,响应体不返回。

创建com.sjb.ResponseOutputStream

public class ResponseOutputStream extends ServletOutputStream {
    public byte[] getBody() {
        return body;
    }
    public int getPos() {
        return pos;
    }
    private byte[] body=new byte[1024];
    private int pos=0;
    @Override
    public void write(int b) throws IOException {
        //响应体
        body[pos++]=(byte)b;
    }
}

3.5 servlet SJBServlet

根据Request创建对应的Response并且创建servlet,调用servlet的service方法,匹配doGet或者doPost

在com.sjb.SocketProcessor#processSocket中

try {
    InputStream inputStream = socket.getInputStream();
    //解析请求头,获得method、url、protocol
    byte[] bytes = new byte[1024];
    int read = inputStream.read(bytes);
    String request = new String(bytes, 0, read);
    String[] split = request.split("\r\n");
    String[] split1 = split[0].split(" ");
    String method = split1[0];
    String url = split1[1];
    String protocol = split1[2];
    Request tomcatrequest = new Request(method, url, protocol, socket);
    //根据url找到对应的servlet
    Response response = new Response(tomcatrequest);
    SJBServlet servlet = new SJBServlet();
    //调用servlet的service方法,匹配doGet或者doPost
    servlet.service(tomcatrequest, response);
    //输出响应
    response.complete();
}

创建com.sjb.SJBServlet,service方法自动给匹配请求方法,我们只需要重写对应的doGet、doPost方法即可

public class SJBServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(req.getMethod());
        resp.addHeader("Content-Type", "text/html;charset=utf-8");
        resp.addHeader("Content-Length", "15");
        resp.getOutputStream().write("Hello World sjb".getBytes());
    }
}

在doGet方法调用response.addHeader()中设置请求头,编码方式、请求体长度等。

也不一定在doGet方法设置请求头,亦可以在com.sjb.Response#sendResponseHeader中设置请求头

private void sendResponseHeader() {
    try {
        if(!headers.containsKey("Content-Type")){
            headers.put("Content-Type", "text/html;charset=utf-8");
        }
        if(!headers.containsKey("Content-Length")){
            headers.put("Content-Length", String.valueOf(responseOutputStream.getPos()));
        }

resp.getOutputStream().write("Hello World sjb".getBytes());设置请求体。

最后调用response.complete();写请求行、请求头、请求体。完成输出流的输出。

3.6 测试

访问

在这里插入图片描述

3.7 Tomcat的部署应用

在tomcat中有一个专门的目录webapps用来存放tomcat的项目,一个tomcat项目中的classes中可以有多个servlet

如果要部署tomcat需要在对应的servlet上加上@WebServlet 注解

在SJBServlet上添加注解

@WebServlet(
    urlPatterns = {"/sjb"}
)
public class SJBServlet extends HttpServlet {
    public SJBServlet() {
    }

@WebServlet 属于类级别的注解,标注在继承了 HttpServlet 的类之上。常用的写法是将 Servlet 的相对请求路径(即 value)直接写在注解内,@WebServlet(urlPatterns = {“/sjb”})。@WebServlet(urlPatterns = {“/sjb”})省略了 urlPatterns 属性名
如果 @WebServlet 中需要设置多个属性,则属性之间必须使用逗号隔开。
通过实现 Serlvet 接口或继承 GenericServlet 创建的 Servlet 类无法使用 @WebServlet 注解。
使用 @WebServlet 注解配置的 Servlet 类,不要在 web.xml 文件中再次配置该 Servlet 相关属性。若同时使用 web.xml 与 @WebServlet 配置同一 Servlet 类,则 web.xml 中 的值与注解中 name 取值不能相同,否则容器会忽略注解中的配置。

3.7.1 @WebServlet 注解 和 web.xml 的区别

使用 web.xml 或 @WebServlet 注解都可以配置 Servlet

  • @WebServlet 注解配置 Servlet

    • 优点:@WebServlet 直接在 Servlet 类中使用,代码量少,配置简单。每个类只关注自身业务逻辑,与其他 Servlet 类互不干扰,适合多人同时开发。
    • 缺点:Servlet 较多时,每个 Servlet 的配置分布在各自的类中,不便于查找和修改。
  • web.xml 配置文件配置 Servlet

    • 优点:集中管理 Servlet 的配置,便于查找和修改。
    • 缺点:代码较繁琐,可读性不强,不易于理解。

3.7.2 目录存放

要将重新编译好的SJBServlet.class放入Tomcat/webapps/hello/classes/com/sjb目录下,并且删除之前的SJBServlet.class

在这里插入图片描述

所以在com.sjb.SocketProcessor#processSocket中就不能直接调用SJBServlet,需要对环境进行解析找到对应的Servlet

3.7.3 部署

在com.sjb.Tomcat#main中需要部署apps

public static void main(String[] args) {
    Tomcat tomcatApplication = new Tomcat();
    tomcatApplication.deployApps();
    tomcatApplication.start();
}

deployApps() :获取当前父项目目录下所有的Tomcat/webapps下的所有目录

private void deployApps() {
    //拿到当前模块的目录的子目录
    //webapps = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps
    File webapps = new File(System.getProperty("user.dir"),"Tomcat/webapps");
    for(String app: webapps.list()){
        //app: hello
        deployApp(webapps,app);
    }
}

针对app: hello进行部署,deployApp(webapps,app):

  • webapps父目录,app子目录
  • context负责存放当前项目。一个context项目下可能有多个app(servlet)
private void deployApp(File webapps,String app) {
    Context context = new Context(app);
    //appDir = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello
    File appDir = new File(webapps, app);
    //classesDir = D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello\classes
    File classesDir = new File(appDir, "classes");
    List<File> files = getAllFilePath(classesDir);
    for(File file:files){
        //name: D:\Code\JavaCode\handwith-Spring\handwith-Spring\Tomcat\webapps\hello\classes\com\sjb\SJBServlet.class
        String name=file.getPath();
		//name: com\sjb\SJBServlet.class
        name=name.replace(classesDir.getPath()+"\\","");
        //name: com\sjb\SJBServlet
        name=name.replace(".class","");
        //name: com.sjb.SJBServlet
        name=name.replace("\\",".");
        System.out.println(name);
        //加载类加载器
        try {
            WebappClassLoader webappClassLoader = new WebappClassLoader(new URL[]{classesDir.toURL()});
            Class<?> aClass = webappClassLoader.loadClass(name);
            if(HttpServlet.class.isAssignableFrom(aClass)){
                if(aClass.isAnnotationPresent(WebServlet.class)){
                    WebServlet webServlet = aClass.getAnnotation(WebServlet.class);
                    //urlPatterns:["/sjb"]
                    String[] urlPatterns = webServlet.urlPatterns();
                    for(String urlPattern:urlPatterns){
                        context.addServlet(urlPattern, (Servlet) aClass.newInstance());        
                    }
                }
            }

        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

    }
    contextMap.put(app,context);

}

List<File> files = getAllFilePath(classesDir);获取hello\classes下所有的文件,递归搜索

private List<File> getAllFilePath(File classesDir) {
        List<File> result = new ArrayList<>();
        File[] files = classesDir.listFiles();
        if(files!=null){
            for(File file:files){
                if(file.isDirectory()){
                    result.addAll(getAllFilePath(file));
                }else{
                    result.add(file);
                }
            }
        }
        return result;
    }

获取到类名name:com.sjb.SJBServlet后,通过类加载器进行加载。

这里不能使用Class<?> aClass1 = Thread.currentThread().getContextClassLoader().loadClass(name);因为这里name是webapps目录下的com.sjb.SJBServlet,而用Thread.currentThread().getContextClassLoader().loadClass()只能加载当前项目下的target目录下的类。aClass.newInstance()通过类加载器new一个对象,和url一起组成Entry放入context下的map里,最后将context放入context的map里

因此需要创建一个新的类加载器com.sjb.WebappClassLoader,继承URLClassLoader,通过url来进行加载

public class WebappClassLoader extends URLClassLoader {
    public WebappClassLoader(URL[] urls) {
        super(urls);
    }
}

HttpServlet.class.isAssignableFrom(aClass)然后再判断这个类是不是HttpServlet,aClass.isAnnotationPresent(WebServlet.class)判断有没有@WebServlet,然后获取注解的内容,并且获取url,,然后把他存入<string,context>的map中,context用来存放每个项目/应用(hello),每个项目中可能有多个servlet

创建com.sjb.Context

public class Context {
    private String appName;
    private Map<String, Servlet> servletMap=new HashMap<>();

    public Context(String appName) {
        this.appName = appName;
    }

    public void addServlet(String url, Servlet servlet){
        servletMap.put(url,servlet);
    }

    public Servlet getServletByUrl(String url){
        return servletMap.get(url);
    }
}

在com.sjb.Tomcat中创建一个Map<String, Context>的map

public class Tomcat {
    public Map<String, Context> getContextMap() {
        return contextMap;
    }

    private Map<String,Context> contextMap=new HashMap<>();

这样就在解析阶段就已经找到了对应的servlet

在com.sjb.SocketProcessor#processSocket中

Request tomcatrequest = new Request(method, url, protocol, socket);
Response response = new Response(tomcatrequest);
//根据url找到对应的servlet
// url: "/hello/sjb"
url=url.substring(1);
// parts: ["abc"]
String[] parts= url.split("/");
String appName=parts[0];
Context context = tomcat.getContextMap().get(appName);
Servlet servlet = context.getServletByUrl("/"+parts[1]);
servlet.service(tomcatrequest, response);

3.7.4 部署测试

访问localhost:8080/hello/sjb,成功访问到

在这里插入图片描述

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

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

相关文章

求阶乘n!末尾0的个数溢出了怎么办

小林最近遇到一个问题&#xff1a;“对于任意给定的一个正整数n&#xff0c;统计其阶乘n&#xff01;的末尾中0的个数”&#xff0c;这个问题究竟该如何解决&#xff1f; 先用n5来解决这个问题。n的阶乘即n!5!5*4*3*2*1120&#xff0c;显然应该为2个数相乘等于10才能得到一个结…

记录minio的bug(Object name contains unsupported characters.)

场景是我将后端服务从121.xxx.xxx.xxx服务器上转移到了另一台服务器10.xxx.xxx.xxx 但图片都还在121.xxx.xxx.xxx服务器上&#xff0c;同样我10.xxx.xxx.xxx也安装了minio并且我的后端服务配置的minio地址也是10.xxx.xxx.xxx 此时有一个业务通过minio客户端获取图片&#xf…

自动化中遇到的问题归纳总结

1、动态元素定位不到 解决方法&#xff1a;尽量使用固定元素定位&#xff0c;如没有固定元素&#xff0c;则采用绝对路径进行定位&#xff0c;因为元素路径是唯一且不变的 2、自动化脚本执行速度较慢 尽量使用css方法定位元素&#xff0c;使用等待时&#xff0c;少用sleep方…

websocket最大数量的限制问题

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; h…

C++类和对象项目:斗地主残局(做为程序员怎么能被人机虐)

斗地主残局解析项目 项目工程化 由于这是一个项目&#xff0c;所以我们需要按照标准的项目工程化来进行设计。 分析大体框架 1. 如何读取牌 如何进行文件读取&#xff0c;可以参看我这篇博客:C语言笔记:文件操作 //创建两个数组进行存储自己和对手的牌 int a[MAX_N 5] {…

一文了解spring事务特性

推荐工具 objectlog 对于重要的一些数据&#xff0c;我们需要记录一条记录的所有版本变化过程&#xff0c;做到持续追踪&#xff0c;为后续问题追踪提供思路。objectlog工具是一个记录单个对象属性变化的日志工具,工具采用spring切面和mybatis拦截器相关技术编写了api依赖包&a…

【Java】HOT100+代码随想录 动态规划(上)背包问题

目录 理论基础 一、基础题目 LeetCode509&#xff1a;斐波那契数 LeetCode70&#xff1a;爬楼梯 LeetCode746&#xff1a;使用最小花费爬楼梯 LeetCode62&#xff1a;不同路径 LeetCode63&#xff1a;不同路径ii LeetCode343&#xff1a;整数拆分 LeetCode96&#xff1a;不…

P8802 [蓝桥杯 2022 国 B] 出差

P8802 [蓝桥杯 2022 国 B] 出差 分析 很明显&#xff1a;单源最短路径 没有负权边 dijkstra 1.存图 2.准备两个数组 dis[]&#xff1a;更新源点到各个点的距离 vis[]&#xff1a;标记是否访问 3.从源点开始&#xff0c;更新源点到与其邻接的点的距离&#xff0c;每次选…

探索 Joomla! CMS:打造个性化网站的利器

上周我们的Hostease客户咨询建站服务。他想要用Joomla建站。Hostease提供免费安装Joomla CMS服务。这可以让客户搭建网站变得更加简单和高效。下面是针对Joomla建站的一些使用心得。 Joomla CMS是一款开放自由的软件&#xff0c;为用户提供了创建和维护网站的自由度。它经过全…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-15.4讲--ARM异常中断返回

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

【已解决】QT C++中QLineEdit不可粘贴输入

本博文源于生产实际&#xff0c;主要解决LineEdit不可粘贴输入的情况。下面将进行具体分析 问题来源 输入框只能一个个输入&#xff0c;不可复制粘贴。 分析 给QLineEdit装一个监听事件&#xff0c;监听它的事件即可。 问题解决步骤 问题一共分为三步&#xff1a; 书写监…

认养小游戏功能介绍

认养小游戏通常模拟了真实的农业生产过程&#xff0c;让玩家能够在线上体验种植、养殖的乐趣。以下是一些常见的认养小游戏功能介绍&#xff1a; 选择认养的农产品&#xff1a;首先&#xff0c;玩家可以从游戏中提供的多种农产品中选择自己想要认养的种类&#xff0c;如蔬菜、…

深入了解模拟和存根:提高单元测试质量的关键技术

一、引言 在进行单元测试时&#xff0c;我们经常会遇到对外部资源的依赖&#xff0c;如数据库、网络接口等。模拟&#xff08;Mocking&#xff09;和存根&#xff08;Stubbing&#xff09;是两种帮助我们模拟这些外部资源&#xff0c;使我们能够在隔离环境中测试单元的方法。在…

PyQt6--Python桌面开发(6.QLineEdit单行文本框)

QLineEdit单行文本框 import sys import time from PyQt6.QtGui import QValidator,QIntValidator from PyQt6.QtWidgets import QApplication,QLabel,QLineEdit from PyQt6 import uicif __name__ __main__:appQApplication(sys.argv)uiuic.loadUi("./QLine单行文本框.u…

618值得入手的平价好物清单,看完再买不吃亏!

即将到来的618年中购物狂欢节&#xff0c;无疑是一年一度的购物盛宴。为了让大家的购物体验更加愉悦和充实&#xff0c;我特地为大家精选了一系列好物。如果你也打算在618尽情购物&#xff0c;那就赶紧收藏这份清单吧&#xff01; 一、舒适佩戴不伤耳——南卡骨传导耳机Runner…

MySQL数据库实验三

本文承接前面的俩次实验基础上完成&#xff0c;不过实现的都是基础操作的练习 目录 目录 前言 实验目的 实验要求 实验内容及步骤 updata操作 delete操作 alter操作 添加列 删除列 修改列的数据类型 要求实现 实验结果 代码结果 注意事项 思考题 总结 前言 本文是MySQL数据库…

有了Supervisor,再也不用担心程序撂挑子了!

目录 1. 概述 2. 问题场景 3. Supervisor 简介 4.部署流程 4.1. 安装 Supervisor 4.2. 自定义服务配置文件 4.3. 自定义应用配置文件 4.4. 启动 supervisord 服务进程 4.5. 启动 supervisorctl 客户端进程 4.6. 验证 supervisor 的监控重启特性 5. 高级特性 5.1. 进…

大模型微调之 在亚马逊AWS上实战LlaMA案例(七)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;七&#xff09; 微调SageMaker JumpStart上的LLaMA 2模型 这是在us-west-2的测试结果。 展示了如何使用SageMaker Python SDK部署预训练的Llama 2模型&#xff0c;并将其微调到你的数据集&#xff0c;用于领域适应或指令…

Python注意事项【自我维护版】

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 本篇博客在之前的博客上进行的维护 创建Python…

Java转Kotlin调用JNI方法异常

一、背景 Java调用JNI方法时没有任何问题&#xff0c;但是使用Java转Kotlin以后出现了崩溃异常&#xff1a;A java_vm_ext.cc:597] JNI DETECTED ERROR IN APPLICATION: jclass has wrong type: 校验参数后没有任何变化&#xff0c;经过分析验证找到解决方案 二、原因…